From e3ea28584bf9c7275dfb8c29333d0edf5c6e4124 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Nov 2025 18:11:10 +0200 Subject: [PATCH 01/30] Updated on 2026-08-14 --- data/visa/detekt-baseline-debug.xml | 1 - detekt_baseline_report.txt | 59 +++++++++---------- domain/wallets/detekt-baseline-debug.xml | 2 - .../account/api/detekt-baseline-debug.xml | 2 - .../account/impl/detekt-baseline-debug.xml | 7 +-- .../details/impl/detekt-baseline-debug.xml | 1 - features/home/impl/detekt-baseline-debug.xml | 3 - .../hot-wallet/impl/detekt-baseline-debug.xml | 4 -- .../markets/impl/detekt-baseline-debug.xml | 7 --- features/nft/impl/detekt-baseline-debug.xml | 2 - .../impl/detekt-baseline-debug.xml | 1 - .../onramp/impl/detekt-baseline-debug.xml | 10 +--- .../referral/impl/detekt-baseline-debug.xml | 2 - .../send-v2/api/detekt-baseline-debug.xml | 1 - .../send-v2/impl/detekt-baseline-debug.xml | 23 -------- .../staking/impl/detekt-baseline-debug.xml | 8 --- .../swap-v2/impl/detekt-baseline-debug.xml | 13 ---- .../swap/domain/detekt-baseline-debug.xml | 1 - features/swap/impl/detekt-baseline-debug.xml | 4 -- .../details/impl/detekt-baseline-debug.xml | 1 - .../impl/detekt-baseline-debug.xml | 1 - .../impl/detekt-baseline-debug.xml | 6 +- .../wallet/impl/detekt-baseline-debug.xml | 18 ------ .../impl/detekt-baseline-debug.xml | 4 -- .../welcome/impl/detekt-baseline-debug.xml | 2 - .../impl/detekt-baseline-debug.xml | 5 -- update_detekt_baseline.sh | 2 +- 27 files changed, 32 insertions(+), 158 deletions(-) diff --git a/data/visa/detekt-baseline-debug.xml b/data/visa/detekt-baseline-debug.xml index bf83464b1c..96bb302f53 100644 --- a/data/visa/detekt-baseline-debug.xml +++ b/data/visa/detekt-baseline-debug.xml @@ -17,7 +17,6 @@ NullableToStringCall:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$${error.message} RedundantSuspendModifier:DefaultVisaRepository.kt$DefaultVisaRepository$suspend SuspendFunSwallowedCancellation:DefaultVisaRepository.kt$DefaultVisaRepository$runCatching - SuspendFunSwallowedCancellation:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$runCatching SuspendFunSwallowedCancellation:VisaApiRequestMaker.kt$VisaApiRequestMaker$runCatching UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$if (status is VisaCardActivationStatus.RefreshTokenExpired) { throw RefreshTokenExpiredException() } UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated") diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 3a9f9a9044..0493bd4f8d 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-11-27 14:13:20 +Date: 2025-11-28 11:36:19 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1593 - Modules with Issues: 68 - Average Issues per Module: 23 + Total Issues: 1473 + Modules with Issues: 65 + Average Issues per Module: 22 Progress: - Fixed: 209 out of 1802 (11%) - Remaining: 1593 + Fixed: 329 out of 1802 (18%) + Remaining: 1473 ========================================== All Modules with Issues (sorted by count) @@ -31,28 +31,28 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 169 -features/markets/impl 155 -features/onboarding-v2/impl 131 -features/send-v2/impl 80 -features/swap/impl 73 -features/hot-wallet/impl 57 -features/staking/impl 56 +features/wallet/impl 151 +features/markets/impl 148 +features/onboarding-v2/impl 130 +features/swap/impl 69 +features/send-v2/impl 57 data/wallet-connect 55 -features/swap-v2/impl 53 -features/walletconnect/impl 51 -features/tokendetails/impl 49 +features/hot-wallet/impl 53 +features/tokendetails/impl 48 +features/staking/impl 48 +features/walletconnect/impl 47 features/manage-tokens/impl 45 -domain/wallets 39 -features/nft/impl 36 +features/swap-v2/impl 40 +domain/wallets 37 +features/nft/impl 34 features/tester/impl 31 domain/tokens 28 -features/swap/domain 27 core/ui 27 -features/yield-supply/impl 26 +features/swap/domain 26 common/ui 26 -data/visa 23 -features/tangempay/details/impl 22 +data/visa 22 +features/yield-supply/impl 21 +features/tangempay/details/impl 21 data/nft 20 data/wallets 18 features/swap/data 15 @@ -63,33 +63,29 @@ domain/account/status 11 data/onramp 11 data/manage-tokens 11 core/datasource 11 -features/referral/impl 10 -features/details/impl 10 data/markets 10 +features/details/impl 9 domain/staking 9 data/yield-supply 9 data/networks 9 -features/welcome/impl 8 -features/home/impl 8 +features/referral/impl 8 domain/transaction 8 libs/tangem-sdk-api 7 -features/onramp/impl 7 data/txhistory 7 data/tokens 7 data/account 7 -features/send-v2/api 6 +features/welcome/impl 6 domain/markets 6 data/wallet-manager 6 libs/visa 5 +features/send-v2/api 5 +features/home/impl 5 domain/legacy 5 data/transaction 5 features/referral/domain 4 features/biometry/impl 4 -features/account/impl 4 -features/account/api 4 data/promo 4 core/config-toggles 4 -features/wallet-settings/impl 3 features/txhistory/impl 3 features/tangempay/onboarding/impl 3 features/create-wallet-start/impl 3 @@ -99,4 +95,5 @@ domain/card 3 data/feedback 3 common/routing 3 features/yield-supply/api 2 +features/account/api 2 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/wallets/detekt-baseline-debug.xml b/domain/wallets/detekt-baseline-debug.xml index 149134a54c..b55688a97f 100644 --- a/domain/wallets/detekt-baseline-debug.xml +++ b/domain/wallets/detekt-baseline-debug.xml @@ -28,8 +28,6 @@ MultilineLambdaItParameter:UpdateWalletUseCase.kt$UpdateWalletUseCase${ when (it) { is SaveWalletError.DataError -> DataError( IllegalStateException("Failed to update wallet: ${it.messageId}"), ) is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists } } NestedScopeFunctions:ColdUserWalletBuilder.kt$ColdUserWalletBuilder$let { UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) } NoNameShadowing:SaveWalletUseCase.kt$SaveWalletUseCase$userWallet - NonBooleanPropertyPrefixedWithIs:GetExtendedPublicKeyForCurrencyUseCase.kt$GetExtendedPublicKeyForCurrencyUseCase$val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey - NonBooleanPropertyPrefixedWithIs:UserWalletsListManagerExtensions.kt$/** * Indicates that the [UserWalletsListManager] is locked * * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which * produces only one false value * * @see UserWalletsListManager.Lockable.isLocked * */ val UserWalletsListManager.isLocked: Flow<Boolean> get() = asLockable()?.lockedState ?: flowOf(false) NullableToStringCall:GenerateBuyTangemCardLinkUseCase.kt$GenerateBuyTangemCardLinkUseCase$$id NullableToStringCall:UpdateWalletUseCase.kt$UpdateWalletUseCase$${it.messageId} ObjectExtendsThrowable:UserWalletsListError.kt$UserWalletsListError$AllKeysInvalidated : UserWalletsListError diff --git a/features/account/api/detekt-baseline-debug.xml b/features/account/api/detekt-baseline-debug.xml index 43b883bfc2..1e04f9d55a 100644 --- a/features/account/api/detekt-baseline-debug.xml +++ b/features/account/api/detekt-baseline-debug.xml @@ -3,8 +3,6 @@ BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$/** * for some Feature specific filtering * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow<Boolean> UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten() diff --git a/features/account/impl/detekt-baseline-debug.xml b/features/account/impl/detekt-baseline-debug.xml index dce6c95e71..ecf2e0cce8 100644 --- a/features/account/impl/detekt-baseline-debug.xml +++ b/features/account/impl/detekt-baseline-debug.xml @@ -1,10 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() } - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true } - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorModel.kt$PortfolioSelectorModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - + diff --git a/features/details/impl/detekt-baseline-debug.xml b/features/details/impl/detekt-baseline-debug.xml index e574008e71..59c8448c09 100644 --- a/features/details/impl/detekt-baseline-debug.xml +++ b/features/details/impl/detekt-baseline-debug.xml @@ -9,7 +9,6 @@ MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) } MultilineLambdaItParameter:PreviewUserWalletListComponent.kt$PreviewUserWalletListComponent${ it.copy( balance = UserWalletItemUM.Balance.Loaded( value = "1.000 BTC", isFlickering = true, ), ) } MultilineLambdaItParameter:UserWalletSaver.kt$UserWalletSaver${ val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } } - NonBooleanPropertyPrefixedWithIs:UserWalletListModel.kt$UserWalletListModel$private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false) RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add) diff --git a/features/home/impl/detekt-baseline-debug.xml b/features/home/impl/detekt-baseline-debug.xml index 684e103473..55111bd85c 100644 --- a/features/home/impl/detekt-baseline-debug.xml +++ b/features/home/impl/detekt-baseline-debug.xml @@ -6,9 +6,6 @@ BooleanPropertyNaming:HomeUM.kt$HomeUM$val scanInProgress: Boolean MultilineLambdaItParameter:HomeModel.kt$HomeModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) } } MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -> it.fillMaxWidth(progress.value) in 0 until currentStep -> it.fillMaxWidth(fraction = 1f) else -> it } } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isFirstStepLaunched = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isLaunched = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isSecondStepLaunched = remember { mutableStateOf(false) } ReusedModifierInstance:HomeButtonsV2.kt$StoriesButton( modifier = modifier, text = stringResourceSafe(id = R.string.common_get_started), useDarkerColors = false, onClick = onGetStartedClick, ) diff --git a/features/hot-wallet/impl/detekt-baseline-debug.xml b/features/hot-wallet/impl/detekt-baseline-debug.xml index 897f428e06..f34cb27c7f 100644 --- a/features/hot-wallet/impl/detekt-baseline-debug.xml +++ b/features/hot-wallet/impl/detekt-baseline-debug.xml @@ -17,11 +17,8 @@ BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM.WordField$val error: Boolean BooleanPropertyNaming:MobileWalletSetupFinishedContent.kt$var showConfetti by remember { mutableStateOf(false) } - BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } - BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } BooleanPropertyNaming:WalletBackupUM.kt$WalletBackupUM$val backedUp: Boolean BooleanPropertyNaming:WalletHardwareBackupUM.kt$WalletHardwareBackupUM$val showPurchaseBlock: Boolean = false - MaxChainedCallsOnSameLine:UpgradeWalletModel.kt$UpgradeWalletModel$it.card.wallets.map { it.curve }.toSet().isNotEmpty() MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ Timber.e(it) setImportProgress(false) } MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ setImportProgress(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), ) } } } MultilineLambdaItParameter:CreateHardwareWalletModel.kt$CreateHardwareWalletModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { router.replaceAll(AppRoute.Wallet) } } } } @@ -48,7 +45,6 @@ MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> WORD_FIELD_INDICES.contains(index + 1) }.toImmutableList(), ) } MultilineLambdaItParameter:ManualBackupPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) } MultilineLambdaItParameter:ManualBackupPhraseModel.kt$ManualBackupPhraseModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) } - MultilineLambdaItParameter:UpgradeWalletModel.kt$UpgradeWalletModel${ // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(it).build() val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null && (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) { startResetCardsFlow.emit(userWallet) return@doOnSuccess } delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) } MultilineLambdaItParameter:ViewPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) } MultilineLambdaItParameter:ViewPhraseModel.kt$ViewPhraseModel${ it.copy( words = words.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) } NoNameShadowing:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy(completeButtonProgress = false) } diff --git a/features/markets/impl/detekt-baseline-debug.xml b/features/markets/impl/detekt-baseline-debug.xml index 58ef1132a3..7c8b95b9d3 100644 --- a/features/markets/impl/detekt-baseline-debug.xml +++ b/features/markets/impl/detekt-baseline-debug.xml @@ -37,7 +37,6 @@ MultilineLambdaItParameter:AddToPortfolioBSContentUMFactory.kt$AddToPortfolioBSContentUMFactory${ if (it != selectedWalletId) { onAnotherWalletSelect(it) onWalletSelectorVisibilityChange(false) } } MultilineLambdaItParameter:AddToPortfolioBottomSheet.kt${ Content( modifier = Modifier.fillMaxWidth(), state = it, ) WalletSelectorBottomSheet(it.walletSelectorConfig) } MultilineLambdaItParameter:AddToPortfolioManager.kt$AddToPortfolioManager${ it.toMutableMap().apply { this[userWalletId] = if (isAddAction) { this[userWalletId].orEmpty() + network } else { this[userWalletId].orEmpty() - network } } } - MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ PortfolioData.CryptoCurrencyData( userWallet = selectedPortfolio.userWallet, status = addedToken, actions = it.states, ) } MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ Timber.e(it) params.callback.onDismiss() } MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ tokenActionsData.emit(it) navigation.replaceAll(AddToPortfolioRoutes.TokenActions) } MultilineLambdaItParameter:AddTokenModel.kt$AddTokenModel${ processError(error = it) uiState.value = um.toggleProgress(false) return@launch } @@ -118,12 +117,6 @@ NoNameShadowing:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel${ it.copy( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.ERROR, ), body = if (it.body is MarketsTokenDetailsUM.Body.Error) { MarketsTokenDetailsUM.Body.Nothing } else { it.body }, ) } NoNameShadowing:MyPortfolioUMFactory.kt$MyPortfolioUMFactory${ networkIds.contains(it.status.currency.network.backendId) } NoNameShadowing:NewMarketsPortfolioDelegate.kt$NewMarketsPortfolioDelegate$portfolio - NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, ) - NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isSearchNotFoundState = batchFlow.state .map { currentSearchText().isNullOrEmpty().not() && it.status is PaginationStatus.EndOfPagination && it.data.isEmpty() } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, ) - NonBooleanPropertyPrefixedWithIs:MarketsListModel.kt$MarketsListModel$val isVisibleOnScreen = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:MarketsListUMStateManager.kt$MarketsListUMStateManager$val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged() - NonBooleanPropertyPrefixedWithIs:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel$val isVisibleOnScreen = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:TokenActionsHandler.kt$TokenActionsHandler$private val isDemoCardUseCase: IsDemoCardUseCase NullableToStringCall:MarketsListItemUM.kt$MarketsListItemUM$marketCap.toString() PropertyUsedBeforeDeclaration:MarketsListModel.kt$MarketsListModel$activeListManager PropertyUsedBeforeDeclaration:MarketsListUMStateManager.kt$MarketsListUMStateManager$state diff --git a/features/nft/impl/detekt-baseline-debug.xml b/features/nft/impl/detekt-baseline-debug.xml index 27148479aa..e079f03d2a 100644 --- a/features/nft/impl/detekt-baseline-debug.xml +++ b/features/nft/impl/detekt-baseline-debug.xml @@ -23,8 +23,6 @@ MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ it.copy( bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), ) } MultilineLambdaItParameter:UpdateDataStateTransformer.kt$UpdateDataStateTransformer${ NFTCollectionUM( id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.rawId), name = it.name.orEmpty(), description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, wrappedList(it.count), ), logoUrl = it.logoUrl, assets = it.transformAssets(), onExpandClick = { onExpandCollectionClick(it) }, isExpanded = it.isExpanded(state), ) } NoNameShadowing:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } - NonBooleanPropertyPrefixedWithIs:NFTCollectionsModel.kt$NFTCollectionsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTReceiveModel.kt$NFTReceiveModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase NullableBooleanCheck:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$(state.content as? NFTCollectionsUM.Content) ?.collections ?.filterIsInstance<NFTCollectionUM>() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false NullableToStringCall:NFTCollectionsContent.kt$${item2?.id} NullableToStringCall:NFTCollectionsModel.kt$NFTCollectionsModel$${network.derivationPath.value} diff --git a/features/onboarding-v2/impl/detekt-baseline-debug.xml b/features/onboarding-v2/impl/detekt-baseline-debug.xml index 3eced7c5d9..7d217909cf 100644 --- a/features/onboarding-v2/impl/detekt-baseline-debug.xml +++ b/features/onboarding-v2/impl/detekt-baseline-debug.xml @@ -107,7 +107,6 @@ NoNameShadowing:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel${ it.copy(resultUserWallet = userWallet) } NoNameShadowing:MultiWalletUpgradeWalletModel.kt$MultiWalletUpgradeWalletModel${ it.copy(resultUserWallet = userWallet) } NoNameShadowing:Wallet1ChooseOptionModel.kt$Wallet1ChooseOptionModel${ it.copy(resultUserWallet = userWallet) } - NonBooleanPropertyPrefixedWithIs:MultiWalletSeedPhraseModel.kt$MultiWalletSeedPhraseModel$private val isWalletAlreadySavedUseCase: IsWalletAlreadySavedUseCase PropertyUsedBeforeDeclaration:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel$onDone RedundantSuspendModifier:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel$suspend RedundantSuspendModifier:OnboardingNoteCreateWalletModel.kt$OnboardingNoteCreateWalletModel$suspend diff --git a/features/onramp/impl/detekt-baseline-debug.xml b/features/onramp/impl/detekt-baseline-debug.xml index 4cc1802ed8..ecf2e0cce8 100644 --- a/features/onramp/impl/detekt-baseline-debug.xml +++ b/features/onramp/impl/detekt-baseline-debug.xml @@ -1,13 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:HotCryptoModel.kt$HotCryptoModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:OnrampAddTokenUiBuilder.kt$OnrampAddTokenUiBuilder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:OnrampMainComponentModel.kt$OnrampMainComponentModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:OnrampOperationModel.kt$OnrampOperationModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:OnrampTokenListModel.kt$OnrampTokenListModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SwapSelectTokensModel.kt$SwapSelectTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - + diff --git a/features/referral/impl/detekt-baseline-debug.xml b/features/referral/impl/detekt-baseline-debug.xml index d0a4690a1f..da51d44c59 100644 --- a/features/referral/impl/detekt-baseline-debug.xml +++ b/features/referral/impl/detekt-baseline-debug.xml @@ -7,8 +7,6 @@ MultilineLambdaItParameter:ReferralScreen.kt${ // TODO: use StateEvent if (stateHolder.errorSnackbar != null) { TangemSnackbar(data = it, actionOnNewLine = true) } else { CopiedTextSnackbar(it) } } MultilineLambdaItParameter:ReferralScreen.kt${ ReferralContent( stateHolder = stateHolder, snackbarHostState = snackbarHostState, onAgreementClick = stateHolder.analytics.onAgreementClicked, modifier = Modifier.padding(it), ) } NamedArguments:ReferralScreen.kt$Text( formatAwardConditionsString( quantity = award, network = networkName, address = if (!address.isNullOrBlank()) " $address" else "", ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT), ) - NonBooleanPropertyPrefixedWithIs:ParticipateBottomBlock.kt$val isExpanded = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:ReferralModel.kt$ReferralModel$private val isDemoCardUseCase: IsDemoCardUseCase SuspendFunSwallowedCancellation:ReferralModel.kt$ReferralModel$runCatching UseOrEmpty:ReferralScreen.kt$matchResult.groups[1]?.value ?: "" VarCouldBeVal:ReferralModel.kt$ReferralModel$private var referralData: MutableStateFlow<ReferralData?> = MutableStateFlow(null) diff --git a/features/send-v2/api/detekt-baseline-debug.xml b/features/send-v2/api/detekt-baseline-debug.xml index 6512d3e5ba..31859862c5 100644 --- a/features/send-v2/api/detekt-baseline-debug.xml +++ b/features/send-v2/api/detekt-baseline-debug.xml @@ -4,7 +4,6 @@ BooleanPropertyNaming:FeeSelectorData.kt$FeeSelectorData$val removeSuggestedFee: Boolean = false BooleanPropertyNaming:SendEntryRoute.kt$SendEntryRoute.ChooseToken$val showSendViaSwapNotification: Boolean - NonBooleanPropertyPrefixedWithIs:SendDestinationComponentParams.kt$SendDestinationComponentParams.DestinationParams$val isBalanceHidingFlow: StateFlow<Boolean> UseEmptyCounterpart:CommonSendAmountAnalyticEvents.kt$CommonSendAmountAnalyticEvents$mapOf() UseEmptyCounterpart:CommonSendAnalyticEvents.kt$CommonSendAnalyticEvents$mapOf() UseEmptyCounterpart:CommonSendFeeAnalyticEvents.kt$CommonSendFeeAnalyticEvents$mapOf() diff --git a/features/send-v2/impl/detekt-baseline-debug.xml b/features/send-v2/impl/detekt-baseline-debug.xml index c88748c497..5c064c3c41 100644 --- a/features/send-v2/impl/detekt-baseline-debug.xml +++ b/features/send-v2/impl/detekt-baseline-debug.xml @@ -46,29 +46,6 @@ NestedScopeFunctions:KaspaCustomFeeConverter.kt$KaspaCustomFeeConverter$let { val valueDecimal = it.value.parseToBigDecimal(it.decimals) // krc-20 transaction will be failed if custom fee value is less than minimum, // so we set value to minimum in this case if (valueDecimal < minimumFee.amount.value) { val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) set( FEE_AMOUNT_INDEX, it.copy( value = fixedValue, label = getFiatReference( rate = currencyStatus.fiatRate, value = valueDecimal, appCurrency = appCurrency, ), ), ) } } NoNameShadowing:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$newFee NoNameShadowing:SendContent.kt$navigationUM - NonBooleanPropertyPrefixedWithIs:FeeSelectorModel.kt$FeeSelectorModel$private val isFeeApproximateUseCase: IsFeeApproximateUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendConfirmComponent.kt$NFTSendConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:NFTSendConfirmModel.kt$NFTSendConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendModel.kt$NFTSendModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendModel.kt$NFTSendModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:NotificationsModel.kt$NotificationsModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams$abstract val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams$abstract val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountBlockParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountBlockParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountModel.kt$SendAmountModel$val isSendWithSwapAvailable: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendConfirmComponent.kt$SendConfirmComponent.Params$val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendConfirmComponent.kt$SendConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendDestinationModel.kt$SendDestinationModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendDestinationModel.kt$SendDestinationModel$private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$val isAccountModeFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) NullCheckOnMutableProperty:SendAmountModel.kt$SendAmountModel$if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 val walletTitle = if (isOnlyOneWallet) { resourceReference(R.string.send_from_title) } else { resourceReference( R.string.send_from_wallet_name, WrappedList(listOf(userWallet?.name.orEmpty())), // TODO AND-11440 ) } _uiState.update { AmountStateConverter( clickIntents = this, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, maxEnterAmount = maxAmountBoundary, iconStateConverter = CryptoCurrencyToIconStateConverter(), isBalanceHidden = params.isBalanceHidingFlow.value, accountTitleUM = AmountAccountConverter( isAccountsMode = isAccountsMode, walletTitle = walletTitle, prefixText = resourceReference(R.string.common_from), ).convert(account), ).convert( AmountParameters( title = walletTitle, value = "", ), ) } } NullableToStringCall:BitcoinCustomFeeConverter.kt$BitcoinCustomFeeConverter$toSatoshiPerByte( amount = feeValue, decimals = value.amount.decimals, txSize = value.txSize, ).toString() NullableToStringCall:NFTSendConfirmModel.kt$NFTSendConfirmModel$params.nftAsset.amount.toString() diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml index 920a160906..2c5de23a8e 100644 --- a/features/staking/impl/detekt-baseline-debug.xml +++ b/features/staking/impl/detekt-baseline-debug.xml @@ -28,14 +28,6 @@ MultilineLambdaItParameter:StakingTransactionSender.kt$StakingTransactionSender${ onConstructError(it) return emptyList() } NoNameShadowing:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader$amount NoNameShadowing:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader${ if (!it.amount.isZero()) return feeResult } - NonBooleanPropertyPrefixedWithIs:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$private val isAccountInitializedProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync( userWalletId = selectedUserWalletId, cryptoCurrency = cryptoCurrency, ).getOrNull() - NonBooleanPropertyPrefixedWithIs:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader$private val isFeeApproximateUseCase: IsFeeApproximateUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:StakingTransactionSender.kt$StakingTransactionSender$private val isFeeApproximateUseCase: IsFeeApproximateUseCase NullCheckOnMutableProperty:StakingModel.kt$StakingModel$if (feeCryptoCurrencyStatus != null && fee != null) { getBalanceNotEnoughForFeeWarningUseCase( fee = fee, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, ).getOrNull() } else { null } NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId diff --git a/features/swap-v2/impl/detekt-baseline-debug.xml b/features/swap-v2/impl/detekt-baseline-debug.xml index 3bc57815b3..d76a6dfe29 100644 --- a/features/swap-v2/impl/detekt-baseline-debug.xml +++ b/features/swap-v2/impl/detekt-baseline-debug.xml @@ -32,19 +32,6 @@ NamedArguments:SwapTransactionSender.kt$SwapTransactionSender$getSwapDataUseCase( userWallet = userWallet, fromCryptoCurrencyStatus = fromStatus, fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), toCryptoCurrency = toStatus.currency, toAddress = destination, expressProvider = provider, rateType = rateType, expressOperationType, ) NoNameShadowing:SendWithSwapContent.kt$navigationUM NoNameShadowing:SwapAmountContent.kt$amountFieldUM - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmComponent.kt$SendWithSwapConfirmComponent.Params$val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmComponent.kt$SendWithSwapConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$val isAccountModeFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams$abstract val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams$abstract val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountBlockParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountBlockParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountParams$override val isBalanceHidingFlow: StateFlow<Boolean> NullableToStringCall:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$error.toString() NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$primaryStatus NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$secondaryStatus diff --git a/features/swap/domain/detekt-baseline-debug.xml b/features/swap/domain/detekt-baseline-debug.xml index 02326b7307..123e48864f 100644 --- a/features/swap/domain/detekt-baseline-debug.xml +++ b/features/swap/domain/detekt-baseline-debug.xml @@ -25,7 +25,6 @@ NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl$account NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl${ it.isAvailable } - NonBooleanPropertyPrefixedWithIs:SwapInteractorImpl.kt$SwapInteractorImpl$private val isDemoCardUseCase: IsDemoCardUseCase NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$$swapData NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$${e.message} SuspendFunSwallowedCancellation:SwapInteractorImpl.kt$SwapInteractorImpl$runCatching diff --git a/features/swap/impl/detekt-baseline-debug.xml b/features/swap/impl/detekt-baseline-debug.xml index e7ed3b9972..d7df904787 100644 --- a/features/swap/impl/detekt-baseline-debug.xml +++ b/features/swap/impl/detekt-baseline-debug.xml @@ -57,10 +57,6 @@ NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus } NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus.currency.id.value == id } NoNameShadowing:SwapModel.kt$SwapModel${ it.key == selectedSwapProvider } - NonBooleanPropertyPrefixedWithIs:StateBuilder.kt$StateBuilder$private val isAccountsModeProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:StateBuilder.kt$StateBuilder$private val isBalanceHiddenProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapModel.kt$SwapModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:TokensDataConverter.kt$TokensDataConverter$private val isBalanceHiddenProvider: Provider<Boolean> NullableToStringCall:SwapModel.kt$SwapModel$${currencyStatus.value.amount} NullableToStringCall:SwapModel.kt$SwapModel$${it.value.amount} NullableToStringCall:TransactionCard.kt$data.toString() diff --git a/features/tangempay/details/impl/detekt-baseline-debug.xml b/features/tangempay/details/impl/detekt-baseline-debug.xml index 8e67fe2685..68aa7140b9 100644 --- a/features/tangempay/details/impl/detekt-baseline-debug.xml +++ b/features/tangempay/details/impl/detekt-baseline-debug.xml @@ -16,7 +16,6 @@ MultilineLambdaItParameter:TangemPayDetailsScreen.kt${ TangemDropdownItem( item = it.dropdownItem, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TangemPayTxHistoryUiManager.kt$TangemPayTxHistoryUiManager${ it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading && it.status !is PaginationStatus.InitialLoadingError } NullCheckOnMutableProperty:GoogleWalletUtil.kt$GoogleWalletUtil$if (walletIntent != null) { walletIntent } else { try { context.packageManager.getLaunchIntentForPackage(WALLET_PACKAGE_NAME) ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } .also { walletIntent = it } } catch (exception: Exception) { Timber.tag(TAG).e(exception) null } } - NullableBooleanCheck:TangemPayDetailsModel.kt$TangemPayDetailsModel$cardDetailsRepository.isAddToWalletDone().getOrNull() ?: false ReusedModifierInstance:DefaultTangemPayDetailsContainerComponent.kt$DefaultTangemPayDetailsContainerComponent$Content(modifier = modifier) ReusedModifierInstance:TangemPayChangePinCodeSuccessScreen.kt$Column( modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { SuccessContent( modifier = Modifier .fillMaxWidth() .weight(1f), ) PrimaryButton( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .padding(bottom = 16.dp) .navigationBarsPadding(), text = stringResourceSafe(R.string.common_done), onClick = onClick, ) } ReusedModifierInstance:TangemPayChangePinScreen.kt$Column( modifier = modifier .fillMaxWidth() .padding(top = 48.dp) .padding(horizontal = 36.dp) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = stringResourceSafe(R.string.visa_onboarding_pin_code_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ) SpacerH16() Text( text = stringResourceSafe(R.string.visa_onboarding_pin_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, ) SpacerH(26.dp) PinCodeSection(state) } diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index 2cd9989be6..bdedae6271 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -31,7 +31,6 @@ NamedArguments:TokenDetailsLoadedBalanceConverter.kt$TokenDetailsLoadedBalanceConverter$formatFiatAmount( status.value, stakingFiatAmount, currentState.selectedBalanceType, appCurrencyProvider(), ) NamedArguments:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$createStateInfo( transaction, toCryptoCurrency, fromCryptoCurrency, toFiatAmount, fromFiatAmount, ) NestedScopeFunctions:TokenDetailsBalanceSelectStateConverter.kt$TokenDetailsBalanceSelectStateConverter$let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } - NonBooleanPropertyPrefixedWithIs:TokenDetailsModel.kt$TokenDetailsModel$private val isDemoCardUseCase: IsDemoCardUseCase NullableBooleanCheck:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$transaction.status?.hasLongTime ?: false NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$networkId NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId diff --git a/features/wallet-settings/impl/detekt-baseline-debug.xml b/features/wallet-settings/impl/detekt-baseline-debug.xml index aba860e5ef..ecf2e0cce8 100644 --- a/features/wallet-settings/impl/detekt-baseline-debug.xml +++ b/features/wallet-settings/impl/detekt-baseline-debug.xml @@ -1,9 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:AccountItemsDelegate.kt$AccountItemsDelegate$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase - + diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index cf87ca958c..7569e2d2a3 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -20,7 +20,6 @@ BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false - BooleanPropertyNaming:WalletModel.kt$WalletModel$val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced() BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content && state.tokensListState is WalletTokensListState.ContentState.PortfolioContent BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount > 0 && !listState.canScrollBackward && !listState.canScrollForward || listState.canScrollBackward && !listState.canScrollForward } } @@ -87,7 +86,6 @@ MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } - MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -> Unit UnlockWalletError.ScannedCardWalletNotMatched -> { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -> { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -> Unit UnlockWalletError.UserWalletNotFound -> { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } MultilineLambdaItParameter:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { it.walletCardState.id } else { null } } NamedArguments:BasicAccountListSubscriber.kt$BasicAccountListSubscriber$updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) @@ -114,23 +112,7 @@ NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NoNameShadowing:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -> Unit UnlockWalletError.ScannedCardWalletNotMatched -> { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -> { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -> Unit UnlockWalletError.UserWalletNotFound -> { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } } NoNameShadowing:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ it is WalletNotification.FinishWalletActivation } - NonBooleanPropertyPrefixedWithIs:AccountDependencies.kt$AccountDependencies$val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase - NonBooleanPropertyPrefixedWithIs:OrganizeTokensModel.kt$OrganizeTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:ScreenLifecycleProvider.kt$ScreenLifecycleProvider$val isBackgroundState: StateFlow<Boolean> = _isBackgroundState - NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase - NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:WalletModel.kt$WalletModel$private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled - NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isAutoScroll = remember { mutableStateOf(value = false) } - NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isNavBarVisible = remember { mutableStateOf(true) } NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} diff --git a/features/walletconnect/impl/detekt-baseline-debug.xml b/features/walletconnect/impl/detekt-baseline-debug.xml index 569bdbc31c..017f85e30e 100644 --- a/features/walletconnect/impl/detekt-baseline-debug.xml +++ b/features/walletconnect/impl/detekt-baseline-debug.xml @@ -31,10 +31,6 @@ NamedArguments:WcSendTransactionModel.kt$WcSendTransactionModel$buildUiState(securityCheck, useCase, signState, isApprovalMethod) NestedScopeFunctions:WcSendAndReceiveBlockAidUiConverter.kt$WcSendAndReceiveBlockAidUiConverter$let { spendAllowanceUMConverter.convert( WcSpendAllowanceUMConverter.Input( approvedAmount = it, onLearnMoreClick = value.onApproveLearnMoreClick, ), ) } NoNameShadowing:WcNavigationUtils.kt$model - NonBooleanPropertyPrefixedWithIs:WcConnectionsModel.kt$WcConnectionsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WcPairModel.kt$WcPairModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WcPortfolioNameDelegate.kt$WcPortfolioNameDelegate$val isAccountMode = isAccountsModeEnabledUseCase.invoke() .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = false) - NonBooleanPropertyPrefixedWithIs:WcRoutingModel.kt$WcRoutingModel$private val isSlotEmpty = MutableStateFlow(true) NullCheckOnMutableProperty:WcCommonTransactionComponentDelegate.kt$WcCommonTransactionComponentDelegate$if (contentStack != null) { val content by contentStack!!.subscribeAsState() BackHandler(onBack = ::onChildBack) content.active.instance.BottomSheet() } NullableBooleanCheck:WcPairModel.kt$WcPairModel$isAccountMode ?: false NullableToStringCall:WcEstimatedWalletChangeUMConverter.kt$WcEstimatedWalletChangeUMConverter$${value.sign} diff --git a/features/welcome/impl/detekt-baseline-debug.xml b/features/welcome/impl/detekt-baseline-debug.xml index eebd20e796..9c54bf2d00 100644 --- a/features/welcome/impl/detekt-baseline-debug.xml +++ b/features/welcome/impl/detekt-baseline-debug.xml @@ -4,10 +4,8 @@ BooleanPropertyNaming:WelcomeModel.kt$WelcomeModel$private var routedOut = false BooleanPropertyNaming:WelcomeUM.kt$WelcomeUM.SelectWallet$val showUnlockWithBiometricButton: Boolean = false - CanBeNonNullable:WelcomeModel.kt$WelcomeModel$specificWalletId: UserWalletId? MultilineLambdaItParameter:WelcomeModel.kt$WelcomeModel${ if (it.isEmpty()) { router.replaceAll(AppRoute.Home()) } wallets.value = it } MultilineLambdaItParameter:WelcomeModel.kt$WelcomeModel${ it.handle( specificWalletId = null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }, ) setSelectWalletState() } - NullableToStringCall:WelcomeModel.kt$WelcomeModel$$specificWalletId ReusedModifierInstance:Welcome.kt$WelcomePlain(modifier = modifier) ReusedModifierInstance:Welcome.kt$WelcomeSelectWallet( state = st, modifier = modifier, ) diff --git a/features/yield-supply/impl/detekt-baseline-debug.xml b/features/yield-supply/impl/detekt-baseline-debug.xml index 1063bc0890..703322ff19 100644 --- a/features/yield-supply/impl/detekt-baseline-debug.xml +++ b/features/yield-supply/impl/detekt-baseline-debug.xml @@ -3,7 +3,6 @@ BooleanPropertyNaming:YieldSupplyApyComponent.kt$YieldSupplyApyComponent$val state by loadingState.collectAsState() - BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$private val handleNavigation = params.handleNavigation BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val processing = uiState.value is YieldSupplyUM.Processing BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend BooleanPropertyNaming:YieldSupplyUM.kt$YieldSupplyUM.Content$val showInfoIcon: Boolean @@ -16,10 +15,6 @@ NamedArguments:YieldSupplyActiveContent.kt$Icon( painterResource(R.drawable.ic_token_info_24), contentDescription = null, modifier = Modifier.size(20.dp), tint = TangemTheme.colors.text.warning, ) NamedArguments:YieldSupplyChartUM.kt$YieldSupplyMarketChartDataUM.Companion$YieldSupplyMarketChartDataUM(y = y, x = x, avr = 5.15, "%.1f") NoNameShadowing:YieldSupplyStopEarningModel.kt$YieldSupplyStopEarningModel$fee - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveComponent.kt$YieldSupplyActiveComponent.Params$val isBalanceHiddenFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveEntryComponent.kt$YieldSupplyActiveEntryComponent.Params$val isBalanceHiddenFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveEntryModel.kt$YieldSupplyActiveEntryModel$val isTransactionInProgressFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:YieldSupplyModel.kt$YieldSupplyModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$tokenPendingStatus NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$tokenProtocolStatus NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$yieldSupplyStatus diff --git a/update_detekt_baseline.sh b/update_detekt_baseline.sh index 1e6de62f27..aa13af5586 100755 --- a/update_detekt_baseline.sh +++ b/update_detekt_baseline.sh @@ -55,7 +55,7 @@ module_count=0 temp_file=$(mktemp) find . -name "detekt-baseline-debug.xml" -type f | while IFS= read -r file; do - issue_count=$(grep -c "" "$file" 2>/dev/null || echo "0") + issue_count=$(grep -c "" "$file" 2>/dev/null) || issue_count=0 if [ "$issue_count" -gt 0 ]; then module_name=$(echo "$file" | sed 's|^\./||' | sed 's|/detekt-baseline-debug.xml$||') From 038cfc6d85719bc132b5527056fb20882b5ea42b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Nov 2025 15:23:47 +0300 Subject: [PATCH 02/30] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsScreen.kt | 134 ++++++++++-------- 1 file changed, 74 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index a81c2bdbd1..47ab70cd44 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -24,6 +24,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 +45,85 @@ 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, + 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, + ) } } From b5ea13145c337d3f0a62bbe54d0f35d087d61843 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Nov 2025 14:39:19 +0400 Subject: [PATCH 03/30] Updated on 2026-08-14 --- .../status/usecase/ManageCryptoCurrenciesUseCase.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 9277ab4837..9e4e862dda 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -87,10 +87,7 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), ) - val isDerivingFailed = derivePublicKeys( - userWalletId = userWalletId, - currencies = modifiedCurrencyList.added, - ).isLeft() + derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { /* @@ -99,12 +96,12 @@ class ManageCryptoCurrenciesUseCase( */ val isOnlyRemoval = modifiedCurrencyList.added.isEmpty() && modifiedCurrencyList.removed.isNotEmpty() - if (isDerivingFailed || isOnlyRemoval) { + if (isOnlyRemoval) { launch { accountsCRUDRepository.syncTokens(userWalletId) } + clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) + return@launch } - if (isDerivingFailed) return@launch - cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) From 22df3286de638777424d7b36d40fac9c02a100f2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 13:19:52 +0300 Subject: [PATCH 04/30] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsScreen.kt | 1 + .../cardsettings/CardSettingsScreenState.kt | 28 +++---------------- .../details/ui/common/utils/ResetToFactory.kt | 2 +- .../details/ui/resetcard/ResetCardScreen.kt | 6 ++-- .../ui/resetcard/ResetCardScreenState.kt | 2 +- .../features/welcome/model/WelcomeModel.kt | 2 +- .../features/welcome/ui/WelcomeScreenState.kt | 2 +- .../welcome/ui/components/WelcomeScreen.kt | 4 +-- 8 files changed, 14 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 47ab70cd44..c5d8bc5cb6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index 5f4415c577..a3422995a8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -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 = 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 - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index cc27cf74c8..d174d82415 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index e37cf7eac6..3e04e9381b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index 32eb289aec..68b8585c72 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 9ca6d43223..0d6b4cf157 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 7c7e43f2b5..5c3ddfe725 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt index de791e45da..50cfcc86b1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt @@ -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 From e79b7e8ecf882c994ccf95beb115b141a6f94ff8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 13:56:29 +0300 Subject: [PATCH 05/30] Updated on 2026-08-14 --- .../repository/UserWalletEncryptionKeysRepository.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index e64db24760..e74499daf0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -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() } } From a787f26800b71dfc7819efa21358930e804654c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 19:19:17 +0700 Subject: [PATCH 06/30] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 ++ core/res/src/main/res/values-ja/strings.xml | 15 ++++++++++++++- core/res/src/main/res/values-ru/strings.xml | 3 ++- core/res/src/main/res/values/strings.xml | 1 + .../account/archived/ArchivedAccountListModel.kt | 2 +- .../account/createedit/AccountCreateEditModel.kt | 2 +- .../account/details/AccountDetailsModel.kt | 12 ++---------- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index bb6de8a19b..95bfe4d9e6 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -830,6 +830,8 @@ Das Zielkonto ist nicht aktiv. Sende %s oder mehr, um das Konto zu aktivieren. Senden Sie Geld an diese Andresse um ein Konto zu erstellen Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset. + Sicher Dir 10 $ in BTC mit jeder Wallet greif schnell zu! + Black Friday: bis zu 30% Nachlass Jetzt beitreten Teile Deinen Code – verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt. Erhalte BELOHNUNGEN für jeden Freund! diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 84d5b13938..b15a7f50b0 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -32,6 +32,7 @@ このアカウントをアーカイブしますが、いつでも復元できます。 アーカイブ中・・・ アカウント + アカウントを編集できません アカウントが保存されました 報酬用のアカウント アカウント番号%s — アドレス導出に使用されます。 @@ -43,6 +44,7 @@ 新しいアカウント アカウントを追加 アカウントを編集 + 少し時間をおいて再度お試しください。問題が続く場合はサポートへご連絡ください。こちらで解決をお手伝いします。 %1$s ( %2$s内) メインアカウント すでにアクティブアカウントの上限%1$s件を超えています。復元するには、1つをアーカイブしてください。 @@ -435,7 +437,7 @@ インターネット接続を確認するか、別のネットワークに切り替えてください。 利用規約 デフォルトアドレス - レガシーアドレス + レガシー%sアドレス 資金を受け取る %sアドレス 他のネットワークで資産を送金すると、永久に失われます。 @@ -1416,9 +1418,20 @@ ポートフォリオ内のあらゆる資産をカードと交換 カードの詳細 カードの一時停止を解除 + 出金 + 現在、出金ができません + 現在の処理が完了するまでは、スワップや新しい出金を開始することはできません。 + 出金処理中です + カードの発行に失敗しました + 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 + 技術的なエラーが発生しました。サポートへお問い合わせください。 通常は最大で15分ほどかかります Tangemカードのセットアップ + カードを発行しています + カードを準備しています。少し時間がかかる場合があります。 Tangem Pay + プロフィールを確認できませんでした。ご不明な点があればサポートまでお問い合わせください。 + 申し訳ございませんが、本人確認を行うことができませんでした ステータスを表示 Tangem PayのKYC手続き進行中 カードをGET diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6288a0dc4c..db0fefa23f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -26,6 +26,7 @@ Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... Аккаунт + Невозможно отредактировать аккаунт Аккаунт для наград Добавить аккаунт Сохранить @@ -831,7 +832,7 @@ Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Для создания аккаунта отправьте средства на этот адрес Аккаунт получателя не содержит трастлайна для отправляемого актива. - Получите $10 в BTC за каждый кошелёк\nПоторопитесь! + Получите $10 в BTC за каждый кошелёк.\nПоторопитесь! Чёрная пятница: скидки до 30% Присоединиться Поделись промокодом — заработай 5 USDT с каждой покупки. Твои друзья получат скидку 10% на карту Tangem! diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 79b1362cb7..4c14322aea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -44,6 +44,7 @@ New account Add account Edit account + Please try again later. If it keeps happening, get in touch with support and we’ll help you resolve it. %1$s in %2$s Main account You have already exceeded the limit of %1$s active accounts. Archive one to recover diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index 67d7387811..c515c6eafd 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -120,7 +120,7 @@ internal class ArchivedAccountListModel @Inject constructor( messageSender.send( DialogMessage( - title = resourceReference(R.string.account_recover_limit_dialog_title), + title = resourceReference(R.string.common_something_went_wrong), message = resourceReference( id = R.string.account_recover_limit_dialog_description, formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 08191c3d3f..6387e99fc8 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -274,7 +274,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun showSomethingWrong() { val dialogMessage = DialogMessage( title = resourceReference(R.string.common_something_went_wrong), - message = resourceReference(R.string.account_could_not_create), + message = resourceReference(R.string.account_generic_error_dialog_message), ) messageSender.send(dialogMessage) } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index 36c4655927..4be0bb85f1 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -104,21 +104,13 @@ internal class AccountDetailsModel @Inject constructor( } private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) { - val titleRes = when (error) { - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, - is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, - -> R.string.common_something_went_wrong - is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, - -> R.string.account_could_not_archive_referral_program_title - } + val titleRes = R.string.common_something_went_wrong val messageRes = when (error) { is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, - -> R.string.account_could_not_archive + -> R.string.account_generic_error_dialog_message is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, -> R.string.account_could_not_archive_referral_program_message } From 1b263fc23482a1adfdba4049b7dcdb03807d52c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 16:16:33 +0300 Subject: [PATCH 07/30] Updated on 2026-08-14 --- .../feature/wallet/child/wallet/WalletComponent.kt | 2 ++ .../wallet/child/wallet/model/WalletModel.kt | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index acd97c7552..da07a45435 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child @@ -48,6 +49,7 @@ internal class WalletComponent @AssistedInject constructor( init { lifecycle.subscribe(model.screenLifecycleProvider) + doOnResume { model.onResume() } componentScope.launch { model.innerWalletRouter.navigateToFlow.collect { navigate(it) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2746942333..6080809e6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -119,13 +119,6 @@ internal class WalletModel @Inject constructor( init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) - screenLifecycleProvider.isBackgroundState - .onEach { isBackground -> - if (isBackground.not()) { - suggestToEnableBiometrics() - } - }.launchIn(modelScope) - suggestToOpenMarkets() maybeMigrateNames() @@ -143,6 +136,12 @@ internal class WalletModel @Inject constructor( clickIntents.initialize(innerWalletRouter, modelScope) } + fun onResume() { + modelScope.launch(dispatchers.main) { + suggestToEnableBiometrics() + } + } + private fun updateYieldSupplyApy() { if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { modelScope.launch(dispatchers.default) { @@ -198,7 +197,6 @@ internal class WalletModel @Inject constructor( private suspend fun shouldShowAskBiometryBottomSheet(): Boolean { return if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && - innerWalletRouter.isWalletLastScreen() && shouldShowAskBiometryUseCase() && canUseBiometryUseCase() } else { From 1a8e50e456ce57735811af73c8828029051ace85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 16:29:40 +0300 Subject: [PATCH 08/30] Updated on 2026-08-14 --- .../start/ui/ManualBackupStartContent.kt | 97 ++++++++++--------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt index 56486d5ef7..fe37f62480 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt @@ -3,9 +3,12 @@ package com.tangem.features.hotwallet.manualbackup.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -22,8 +25,8 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier + Box( + modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .padding( @@ -33,53 +36,57 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi bottom = 16.dp, ), ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp, + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe(R.string.backup_info_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe( + R.string.backup_info_description, + state.seepPhraseLength.toString(), ), - text = stringResourceSafe(R.string.backup_info_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_save_title), + description = stringResourceSafe( + R.string.backup_info_save_description, + state.seepPhraseLength.toString(), ), - text = stringResourceSafe( - R.string.backup_info_description, - state.seepPhraseLength.toString(), - ), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - FeatureBlock( - modifier = Modifier - .padding(top = 24.dp), - title = stringResourceSafe(R.string.backup_info_save_title), - description = stringResourceSafe( - R.string.backup_info_save_description, - state.seepPhraseLength.toString(), - ), - iconRes = R.drawable.ic_lock_24, - ) - FeatureBlock( - modifier = Modifier - .padding(top = 24.dp), - title = stringResourceSafe(R.string.backup_info_keep_title), - description = stringResourceSafe(R.string.backup_info_keep_description), - iconRes = R.drawable.ic_settings_24, - ) - Spacer(modifier = Modifier.weight(1f)) + iconRes = R.drawable.ic_lock_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_keep_title), + description = stringResourceSafe(R.string.backup_info_keep_description), + iconRes = R.drawable.ic_settings_24, + ) + Spacer(modifier = Modifier.weight(1f)) + } + PrimaryButton( modifier = Modifier + .align(Alignment.BottomCenter) .fillMaxWidth() .padding(top = 16.dp), text = stringResourceSafe(R.string.common_continue), From 23ea022a94f5354d4e82f00720630612ab080f0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 16:37:27 +0300 Subject: [PATCH 09/30] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 1d456f1591..b6621479a7 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -33,7 +33,7 @@ }, { "name": "HOT_WALLET_ENABLED", - "version": "undefined" + "version": "5.32.0" }, { "name": "TANGEM_PAY_ENABLED", From 79d96d3b8d21937f931bbdba794d932ad19e3bd5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 16:41:51 +0300 Subject: [PATCH 10/30] Updated on 2026-08-14 --- .../features/hotwallet/upgradewallet/UpgradeWalletModel.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index 976804e17d..de7cbe111f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -18,7 +18,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.card.BackupValidator import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -131,12 +130,9 @@ internal class UpgradeWalletModel @Inject constructor( scanResponse: ScanResponse, onSuccess: suspend () -> Unit, ) { - // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() - val isSameWalletButNotFinishedBackup = userWallet?.walletId == params.userWalletId && - BackupValidator.isValidFull(scanResponse.card).not() - if (userWallet != null && isSameWalletButNotFinishedBackup) { + if (userWallet?.walletId == params.userWalletId) { startResetCardsFlow.emit(userWallet) return } From b2be9ebab0b79ac437d50d3420922a68b7d736b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 17:35:35 +0300 Subject: [PATCH 11/30] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 60e77f7ace..b3a3fbf1a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ + android:required="false" /> From 340bc656c7d5713de69685d056ddfb88fec699bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Dec 2025 18:59:49 +0400 Subject: [PATCH 12/30] Updated on 2026-08-14 --- core/config-toggles/detekt-baseline-debug.xml | 2 -- .../impl/DevExcludedBlockchainsManager.kt | 2 ++ .../feature/impl/DevFeatureTogglesManager.kt | 2 ++ .../fetcher/DefaultWalletAccountsFetcher.kt | 21 ++++++++++++--- .../DefaultMainAccountTokensMigration.kt | 2 +- .../data/common/network/NetworkFactory.kt | 4 ++- data/feedback/detekt-baseline-debug.xml | 2 -- .../feedback/DefaultFeedbackRepository.kt | 6 ++--- .../converters/BlockchainInfoConverter.kt | 4 ++- data/promo/detekt-baseline-debug.xml | 2 -- detekt_baseline_report.txt | 27 +++++++++---------- domain/card/detekt-baseline-debug.xml | 9 ------- .../domain/card/TangemCardTypesResolver.kt | 23 +++++++++------- .../domain/card/common/TapWorkarounds.kt | 1 + domain/demo/models/detekt-baseline-debug.xml | 9 ------- .../tangem/domain/demo/models/DemoConfig.kt | 5 +--- .../swap/domain/detekt-baseline-debug.xml | 1 - features/swap/impl/detekt-baseline-debug.xml | 2 -- .../DefaultTokenDetailsComponent.kt | 2 +- .../wallet/impl/detekt-baseline-debug.xml | 3 --- .../api/detekt-baseline-debug.xml | 8 ------ .../yield/supply/api/YieldSupplyComponent.kt | 2 +- .../api/analytics/YieldSupplyAnalytics.kt | 2 +- .../impl/main/model/YieldSupplyModel.kt | 9 ++----- 24 files changed, 63 insertions(+), 87 deletions(-) delete mode 100644 domain/card/detekt-baseline-debug.xml delete mode 100644 domain/demo/models/detekt-baseline-debug.xml delete mode 100644 features/yield-supply/api/detekt-baseline-debug.xml diff --git a/core/config-toggles/detekt-baseline-debug.xml b/core/config-toggles/detekt-baseline-debug.xml index b0e03ef827..274a75d8d4 100644 --- a/core/config-toggles/detekt-baseline-debug.xml +++ b/core/config-toggles/detekt-baseline-debug.xml @@ -2,8 +2,6 @@ - DoubleMutabilityForCollection:DevExcludedBlockchainsManager.kt$DevExcludedBlockchainsManager$private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull() - DoubleMutabilityForCollection:DevFeatureTogglesManager.kt$DevFeatureTogglesManager$private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull() Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ Indentation:FeatureToggles.kt$FeatureToggles$ diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt index 6f6645c865..98311b8d7a 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt @@ -21,6 +21,8 @@ internal class DevExcludedBlockchainsManager( ) : MutableExcludedBlockchainsManager { private val fileBlockchainToggles: Map = getFileBlockchainToggles() + + @Suppress("DoubleMutabilityForCollection") private var blockchainTogglesMap: MutableMap by Delegates.notNull() override val excludedBlockchainsIds: Set diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 79d78151b7..d59bac9d71 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -22,6 +22,8 @@ internal class DevFeatureTogglesManager( ) : MutableFeatureTogglesManager { private val fileFeatureTogglesMap: Map = getFileFeatureToggles() + + @Suppress("DoubleMutabilityForCollection") private var featureTogglesMap: MutableMap by Delegates.notNull() init { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 99d4ac7ff6..3bc99dc784 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -115,7 +115,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( saveETag(userWalletId, apiResponse) - apiResponse.bind() + apiResponse.bind().enrichByAccountId() }, onError = { error -> if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) { @@ -142,10 +142,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 @@ -215,6 +216,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) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index d1a0449ebc..d4e9d0325f 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -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 } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index 4ffb1c9210..265418339e 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -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( diff --git a/data/feedback/detekt-baseline-debug.xml b/data/feedback/detekt-baseline-debug.xml index 596e50e191..d10388bff4 100644 --- a/data/feedback/detekt-baseline-debug.xml +++ b/data/feedback/detekt-baseline-debug.xml @@ -3,7 +3,5 @@ BooleanPropertyNaming:DefaultFeedbackRepository.kt$DefaultFeedbackRepository$private val useNewUserWalletsRepository: Boolean - MultilineLambdaItParameter:DefaultFeedbackRepository.kt$DefaultFeedbackRepository${ it.toMutableMap().apply { put(userWallet.walletId, error) } } - UseOrEmpty:BlockchainInfoConverter.kt$BlockchainInfoConverter$value.wallet.publicKey.derivationPath?.rawPath ?: "" diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 10bb463509..dcadace780 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -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 } } } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt index 686cec71dd..8c430ced24 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt @@ -16,9 +16,11 @@ import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainA internal object BlockchainInfoConverter : Converter { 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), diff --git a/data/promo/detekt-baseline-debug.xml b/data/promo/detekt-baseline-debug.xml index 0f6f378450..7ac3d42cb4 100644 --- a/data/promo/detekt-baseline-debug.xml +++ b/data/promo/detekt-baseline-debug.xml @@ -2,8 +2,6 @@ - NullableBooleanCheck:DefaultPromoRepository.kt$DefaultPromoRepository$getSepaPromoBanner()?.isActive ?: false - NullableBooleanCheck:DefaultPromoRepository.kt$DefaultPromoRepository$getVisaPromoBanner()?.isActive ?: false SuspendFunSwallowedCancellation:DefaultPromoRepository.kt$DefaultPromoRepository$runCatching SuspendFunWithFlowReturnType:DefaultPromoRepository.kt$DefaultPromoRepository$suspend diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 0493bd4f8d..6e74d6e47d 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-11-28 11:36:19 +Date: 2025-12-01 16:30:16 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1473 - Modules with Issues: 65 - Average Issues per Module: 22 + Total Issues: 1453 + Modules with Issues: 62 + Average Issues per Module: 23 Progress: - Fixed: 329 out of 1802 (18%) - Remaining: 1473 + Fixed: 349 out of 1802 (19%) + Remaining: 1453 ========================================== All Modules with Issues (sorted by count) @@ -31,10 +31,10 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 151 +features/wallet/impl 148 features/markets/impl 148 features/onboarding-v2/impl 130 -features/swap/impl 69 +features/swap/impl 67 features/send-v2/impl 57 data/wallet-connect 55 features/hot-wallet/impl 53 @@ -48,8 +48,8 @@ features/nft/impl 34 features/tester/impl 31 domain/tokens 28 core/ui 27 -features/swap/domain 26 common/ui 26 +features/swap/domain 25 data/visa 22 features/yield-supply/impl 21 features/tangempay/details/impl 21 @@ -84,16 +84,13 @@ domain/legacy 5 data/transaction 5 features/referral/domain 4 features/biometry/impl 4 -data/promo 4 -core/config-toggles 4 features/txhistory/impl 3 features/tangempay/onboarding/impl 3 features/create-wallet-start/impl 3 domain/manage-tokens 3 -domain/demo/models 3 -domain/card 3 -data/feedback 3 common/routing 3 -features/yield-supply/api 2 features/account/api 2 +data/promo 2 +core/config-toggles 2 +data/feedback 1 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/card/detekt-baseline-debug.xml b/domain/card/detekt-baseline-debug.xml deleted file mode 100644 index 277a01c1fe..0000000000 --- a/domain/card/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - NamedArguments:TangemCardTypesResolver.kt$TangemCardTypesResolver$Token( cardToken.name, cardToken.symbol, cardToken.contractAddress, cardToken.decimals, ) - PropertyUsedBeforeDeclaration:TapWorkarounds.kt$TapWorkarounds$excludedBatches - PropertyUsedBeforeDeclaration:TapWorkarounds.kt$TapWorkarounds$excludedIssuers - - diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt index db4b0973cb..3e6943f724 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt @@ -36,15 +36,15 @@ internal class TangemCardTypesResolver( override fun isTangemWallet(): Boolean { return card.settings.isBackupAllowed && card.settings.isHDWalletAllowed && - card.firmwareVersion >= FirmwareVersion.Companion.MultiWalletAvailable + card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable } override fun isShibaWallet(): Boolean { - return card.firmwareVersion.compareTo(FirmwareVersion.Companion.KeysImportAvailable) == 0 + return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0 } override fun isWhiteWallet(): Boolean { - return walletData == null && card.firmwareVersion <= FirmwareVersion.Companion.HDWalletAvailable + return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } override fun isWallet2(): Boolean = card.isWallet2 @@ -73,7 +73,7 @@ internal class TangemCardTypesResolver( (multiWalletAvailable() || card.wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1) } - private fun multiWalletAvailable() = card.firmwareVersion >= FirmwareVersion.Companion.MultiWalletAvailable + private fun multiWalletAvailable() = card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable override fun getBlockchain(): Blockchain { return when (productType) { @@ -93,12 +93,15 @@ internal class TangemCardTypesResolver( override fun getPrimaryToken(): Token? { val cardToken = walletData?.token ?: return null - return Token( - cardToken.name, - cardToken.symbol, - cardToken.contractAddress, - cardToken.decimals, - ) + + return with(cardToken) { + Token( + name = name, + symbol = symbol, + contractAddress = contractAddress, + decimals = decimals, + ) + } } override fun isReleaseFirmwareType(): Boolean = card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/TapWorkarounds.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/TapWorkarounds.kt index 550ddd0749..f012a0bfaf 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/TapWorkarounds.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/TapWorkarounds.kt @@ -38,6 +38,7 @@ object TapWorkarounds { val CardDTO.hasOldStyleDerivation: Boolean get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" + @Suppress("PropertyUsedBeforeDeclaration") val CardDTO.isExcluded: Boolean get() { val isBatchExcluded = excludedBatches.contains(batchId) diff --git a/domain/demo/models/detekt-baseline-debug.xml b/domain/demo/models/detekt-baseline-debug.xml deleted file mode 100644 index 638c5c1870..0000000000 --- a/domain/demo/models/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - PropertyUsedBeforeDeclaration:DemoConfig.kt$DemoConfig$debugTestDemoCardIds - PropertyUsedBeforeDeclaration:DemoConfig.kt$DemoConfig$releaseDemoCardIds - PropertyUsedBeforeDeclaration:DemoConfig.kt$DemoConfig$testDemoCardIds - - diff --git a/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfig.kt b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfig.kt index 0ec194c730..5ed3b8e893 100644 --- a/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfig.kt +++ b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfig.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import java.math.BigDecimal -@Suppress("LargeClass") +@Suppress("LargeClass", "ClassOrdering", "PropertyUsedBeforeDeclaration") object DemoConfig { /** @@ -65,7 +65,6 @@ object DemoConfig { return (releaseDemoCardIds + testDemoCardIds).distinct() } - @Suppress("ClassOrdering") private val releaseDemoCardIds = mutableListOf( // === Not from the Google Sheet table === "AC01000000041225", @@ -449,7 +448,6 @@ object DemoConfig { "AF10100000000084", ) - @Suppress("ClassOrdering") private val testDemoCardIds = listOf( "FB20000000000186", // Note ETH "FB10000000000196", // Note BTC @@ -457,6 +455,5 @@ object DemoConfig { "FB04000000000152", // Wallet 2 ) - @Suppress("ClassOrdering") private val debugTestDemoCardIds = emptyList() } \ No newline at end of file diff --git a/features/swap/domain/detekt-baseline-debug.xml b/features/swap/domain/detekt-baseline-debug.xml index 123e48864f..e1d3527ea0 100644 --- a/features/swap/domain/detekt-baseline-debug.xml +++ b/features/swap/domain/detekt-baseline-debug.xml @@ -25,7 +25,6 @@ NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl$account NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl${ it.isAvailable } - NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$$swapData NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$${e.message} SuspendFunSwallowedCancellation:SwapInteractorImpl.kt$SwapInteractorImpl$runCatching diff --git a/features/swap/impl/detekt-baseline-debug.xml b/features/swap/impl/detekt-baseline-debug.xml index d7df904787..72441faba2 100644 --- a/features/swap/impl/detekt-baseline-debug.xml +++ b/features/swap/impl/detekt-baseline-debug.xml @@ -48,8 +48,6 @@ MultilineLambdaItParameter:SwapModel.kt$SwapModel${ uiState = stateBuilder.dismissBottomSheet(uiState) dataState = dataState.copy(selectedFee = it) modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } } MultilineLambdaItParameter:SwapModel.kt$SwapModel${ val balance = swapInteractor.getTokenBalance(it) onAmountChanged(balance.formatToUIRepresentation()) } MultilineLambdaItParameter:SwapModel.kt$SwapModel${ val provider = findAndSelectProvider(it) val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.dismissBottomSheet(uiState) setupLoadedState( provider = provider, state = swapState, fromToken = fromToken, ) } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ when (it) { is SwapTransactionState.TxSent -> { sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) updateWalletBalance() uiState = stateBuilder.loadingPermissionState(uiState) uiState = stateBuilder.dismissBottomSheet(uiState) startLoadingQuotesFromLastState(isSilent = true) } is SwapTransactionState.Error -> { uiState = stateBuilder.createErrorTransactionAlert( uiState = uiState, error = it, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onSupportClick = ::onFailedTxEmailClick, isReverseSwapPossible = isReverseSwapPossible(), ) } SwapTransactionState.DemoMode -> { uiState = stateBuilder.createDemoModeAlert( uiState = uiState, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, isReverseSwapPossible = isReverseSwapPossible(), ) } } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ when (it) { is SwapTransactionState.TxSent -> { sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) val url = getExplorerTransactionUrlUseCase( txHash = it.txHash, networkId = fromCurrency.currency.network.id, ).getOrElse { Timber.i("tx hash explore not supported") "" } updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, swapTransactionState = it, dataState = dataState, txUrl = url, onExploreClick = { if (it.txHash.isNotEmpty()) { urlOpener.openUrl(url) } analyticsEventHandler.send( event = SwapEvents.ButtonExplore(initialCurrencyFrom.symbol), ) }, onStatusClick = { val txExternalUrl = it.txExternalUrl if (!txExternalUrl.isNullOrBlank()) { urlOpener.openUrl(txExternalUrl) analyticsEventHandler.send( event = SwapEvents.ButtonStatus(initialCurrencyFrom.symbol), ) } }, ) sendSuccessEvent() swapRouter.openScreen(SwapNavScreen.Success) } SwapTransactionState.DemoMode -> { uiState = stateBuilder.createDemoModeAlert( uiState = uiState, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, isReverseSwapPossible = isReverseSwapPossible(), ) } is SwapTransactionState.Error -> { startLoadingQuotesFromLastState() uiState = stateBuilder.createErrorTransactionAlert( uiState = uiState, error = it, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onSupportClick = ::onFailedTxEmailClick, isReverseSwapPossible = isReverseSwapPossible(), ) } } } MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier .align(Alignment.CenterVertically) .testTag(SwapTokenScreenTestTags.BALANCE), ) } MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 4f4dbe32e0..8c3ca7e009 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -77,7 +77,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( params = YieldSupplyComponent.Params( userWalletId = params.userWalletId, cryptoCurrency = params.currency, - handleNavigation = (params.navigationAction as? NavigationAction.YieldSupply) + shouldHandleNavigation = (params.navigationAction as? NavigationAction.YieldSupply) ?.isActive, ), ) diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index 7569e2d2a3..c3993c7c7e 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -5,7 +5,6 @@ BooleanPropertyNaming:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher$@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem$abstract val showShadow: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem.RoundingMode$abstract val showGap: Boolean - BooleanPropertyNaming:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$val accessCodeSkipped = array[7] as Boolean BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private var readyForRateAppNotification = false BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$val userHasWalletOrWallet2 = userWallets.filterIsInstance<UserWallet.Cold>().any { val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } BooleanPropertyNaming:OrganizeTokensState.kt$OrganizeTokensState.ActionsConfig$val showApplyProgress: Boolean = false @@ -77,7 +76,6 @@ MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } MultilineLambdaItParameter:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase${ val defaultName = it.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) } Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) } - MultilineLambdaItParameter:WalletNotifications.kt${ // TODO develop promo banner general component when (it) { is WalletNotification.SwapPromo -> { // Use it on new promo action } is WalletNotification.NoteMigration -> { NoteMigrationNotification( config = it.config, modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), ) } is WalletNotification.FinishWalletActivation -> { Notification( config = it.config, modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), ) } else -> { Notification( config = it.config, modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), iconTint = when (it) { is WalletNotification.Critical -> TangemTheme.colors.icon.warning is WalletNotification.Informational -> TangemTheme.colors.icon.accent is WalletNotification.RateApp -> TangemTheme.colors.icon.attention is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) } } } MultilineLambdaItParameter:WalletScreen.kt${ PaddingValues( bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, ) } MultilineLambdaItParameter:WalletScreen.kt${ WalletSnackbarHost( snackbarHostState = it, event = state.event, modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing4) .navigationBarsPadding(), ) } MultilineLambdaItParameter:WalletScreen.kt${ balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, ) } @@ -89,7 +87,6 @@ MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } MultilineLambdaItParameter:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { it.walletCardState.id } else { null } } NamedArguments:BasicAccountListSubscriber.kt$BasicAccountListSubscriber$updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents, accessCodeSkipped) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) NamedArguments:TangemSnapFlingBehavior.kt$HighVelocityApproachAnimation$animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep) diff --git a/features/yield-supply/api/detekt-baseline-debug.xml b/features/yield-supply/api/detekt-baseline-debug.xml deleted file mode 100644 index 2c1d5f69ba..0000000000 --- a/features/yield-supply/api/detekt-baseline-debug.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - BooleanPropertyNaming:YieldSupplyComponent.kt$YieldSupplyComponent.Params$val handleNavigation: Boolean? = null - UseEmptyCounterpart:YieldSupplyAnalytics.kt$YieldSupplyAnalytics$mapOf() - - diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt index c2f5cf26f0..d1919f0781 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyComponent.kt @@ -10,7 +10,7 @@ interface YieldSupplyComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, - val handleNavigation: Boolean? = null, + val shouldHandleNavigation: Boolean? = null, ) interface Factory : ComponentFactory diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index 942e3a3d8a..37b00d61b2 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -8,7 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class YieldSupplyAnalytics( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = "Earning", event = event, params = params) { data class EarningScreenInfoOpened( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 650ba7d090..900c7fecd8 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -24,11 +24,7 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus -import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase +import com.tangem.domain.yield.supply.usecase.* import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.R @@ -271,8 +267,7 @@ internal class YieldSupplyModel @Inject constructor( private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend - val state = uiState.value - val isShowInfoIconPrevState = when (state) { + val isShowInfoIconPrevState = when (val state = uiState.value) { is YieldSupplyUM.Content -> state.showInfoIcon else -> false } From 73cd58dc662b0e324d1aefb3093b7098f0e357bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 14:59:52 +0700 Subject: [PATCH 13/30] Updated on 2026-08-14 --- .../createedit/ui/AccountCreateEditContent.kt | 6 ++++-- .../subscribers/BasicAccountListSubscriber.kt | 17 ++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 57adf472ad..4828371761 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -30,6 +30,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.R import com.tangem.common.ui.account.* import com.tangem.core.ui.components.PrimaryButton @@ -188,7 +190,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { .padding(contentPadding), horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - colorsState.list.forEach { color -> + colorsState.list.fastForEach { color -> val isSelected = color == colorsState.selected Box( contentAlignment = Alignment.Center, @@ -237,7 +239,7 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { maxItemsInEachRow = 6, modifier = Modifier.fillMaxWidth(), ) { - iconsState.list.forEachIndexed { index, icon -> + iconsState.list.fastForEachIndexed { index, icon -> val isSelected = icon == iconsState.selected Box( contentAlignment = Alignment.Center, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 40a9681b13..c8e072d004 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -49,7 +49,6 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), ) { - val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount when { @@ -70,20 +69,8 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ) } isAccountMode -> { - val isAllAccountsEmpty = accountFlattenCurrencies.isEmpty() - if (isAllAccountsEmpty) { - stateController.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = TokenListError.EmptyTokens, - appCurrency = appCurrency, - ), - ) - } else { - val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - - updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) - } + val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) + updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) } } } From 762caca32e367d7f7f5138ba7ac792530e41fee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 16:36:03 +0700 Subject: [PATCH 14/30] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 2 + .../usecase/RecoverCryptoPortfolioUseCase.kt | 12 +++++ .../RecoverCryptoPortfolioUseCaseTest.kt | 49 +++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 2afd20f31a..80d510d122 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -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, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index d2eb7a9b1c..c972f97b80 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -7,6 +7,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository @@ -23,6 +24,8 @@ import com.tangem.domain.models.wallet.UserWalletId * * @property crudRepository repository for performing CRUD operations on accounts * @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account + * @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances. + * @property singleAccountListFetcher fetches the list of accounts for a single user wallet * [REDACTED_AUTHOR] */ @@ -30,6 +33,7 @@ class RecoverCryptoPortfolioUseCase( private val crudRepository: AccountsCRUDRepository, private val mainAccountTokensMigration: MainAccountTokensMigration, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, + private val singleAccountListFetcher: SingleAccountListFetcher, ) { /** @@ -38,6 +42,8 @@ class RecoverCryptoPortfolioUseCase( * @param accountId the unique identifier of the account to recover */ suspend operator fun invoke(accountId: AccountId): Either = either { + fetchAccountList(userWalletId = accountId.userWalletId) + val accountList = getAccountList(userWalletId = accountId.userWalletId) ensure(accountList.canAddMoreAccounts) { @@ -62,6 +68,12 @@ class RecoverCryptoPortfolioUseCase( recoveredAccount } + private suspend fun Raise.fetchAccountList(userWalletId: UserWalletId) { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId)).onLeft { + raise(Error.DataOperationFailed(cause = it)) + } + } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt index f6fe05e41d..8bf3eb7e56 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -5,6 +5,7 @@ import arrow.core.left import arrow.core.right import arrow.core.toOption import com.google.common.truth.Truth +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository @@ -27,17 +28,19 @@ import kotlin.random.Random class RecoverCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val singleAccountListFetcher: SingleAccountListFetcher = mockk() private val mainAccountTokensMigration: MainAccountTokensMigration = mockk() private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher = mockk(relaxUnitFun = true) private val useCase = RecoverCryptoPortfolioUseCase( crudRepository = crudRepository, mainAccountTokensMigration = mainAccountTokensMigration, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, + singleAccountListFetcher = singleAccountListFetcher, ) @BeforeEach fun resetMocks() { - clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher) + clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher, singleAccountListFetcher) } @Test @@ -56,6 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right() @@ -69,6 +73,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) @@ -85,6 +90,7 @@ class RecoverCryptoPortfolioUseCaseTest { derivationIndex = DerivationIndex.Companion.Main, ) + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns None // Act @@ -95,7 +101,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) - coVerifySequence { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -111,6 +118,7 @@ class RecoverCryptoPortfolioUseCaseTest { ) val exception = IllegalStateException("Test error") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act @@ -120,7 +128,8 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifySequence { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -134,6 +143,7 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.Companion.empty(userWalletId) val exception = IllegalStateException("Test error") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } throws exception @@ -145,6 +155,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -157,6 +168,7 @@ class RecoverCryptoPortfolioUseCaseTest { val account = createAccount(userWalletId) val accountList = AccountList.Companion.empty(userWalletId) + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None @@ -169,6 +181,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -192,6 +205,7 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception @@ -204,12 +218,41 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) } } + @Test + fun `invoke should return error if fetch is failed`() = runTest { + // Arrange + val accountId = AccountId.Companion.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Companion.Main, + ) + val exception = IllegalStateException("Test error") + + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns exception.left() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } + coVerify(inverse = true) { + crudRepository.getAccountListSync(any()) + crudRepository.getArchivedAccountSync(any()) + crudRepository.saveAccounts(any()) + } + } + private fun createAccount( userWalletId: UserWalletId, name: String = "Test Account", From 420eb2b2460dfeb0546de905258c2aa7d84f8d57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 14:51:52 +0500 Subject: [PATCH 15/30] Updated on 2026-08-14 --- .../com/tangem/tap/network/auth/DefaultAuthProvider.kt | 4 +++- .../datasource/api/common/config/ApiEnvironment.kt | 3 +++ .../com/tangem/datasource/api/common/config/Express.kt | 7 +++++++ .../tangem/datasource/api/common/config/YieldSupply.kt | 1 + .../repository/DefaultTangemPayCardDetailsRepository.kt | 1 + .../tangem/data/visa/DefaultVisaActivationRepository.kt | 1 + .../environments/utils/ApiEnvironmentComparator.kt | 9 +++++---- 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index f873c4683f..597915ff66 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -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") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt index 755887fd7f..f976466d89 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt @@ -22,6 +22,9 @@ enum class ApiEnvironment { @Json(name = "STAGE") STAGE, + @Json(name = "STAGE_2") + STAGE_2, + @Json(name = "MOCK") MOCK, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 6d4b9a1e5a..4104dae025 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -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]", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index 6b7d1ab493..b5e7ea38c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -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") diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 12c3438e17..eb8ce5def8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -189,6 +189,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, ApiEnvironment.STAGE, + ApiEnvironment.STAGE_2, ApiEnvironment.MOCK, -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev ApiEnvironment.PROD -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.prod diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index a227353e33..5b5ee71abe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -171,6 +171,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, ApiEnvironment.STAGE, + ApiEnvironment.STAGE_2, ApiEnvironment.MOCK, -> rsaPublicKey.dev ApiEnvironment.PROD -> rsaPublicKey.prod diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt index ac81103c06..69b0648f7d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt @@ -10,14 +10,15 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig */ internal object ApiEnvironmentComparator : Comparator { - private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith { - when (it) { + private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith { environment -> + when (environment) { ApiEnvironment.DEV -> 0 ApiEnvironment.DEV_2 -> 1 ApiEnvironment.DEV_3 -> 2 ApiEnvironment.STAGE -> 3 - ApiEnvironment.MOCK -> 4 - ApiEnvironment.PROD -> 5 + ApiEnvironment.STAGE_2 -> 4 + ApiEnvironment.MOCK -> 5 + ApiEnvironment.PROD -> 6 } } From 683525570cab067ae1afb4b7611afae2c9910329 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 14:06:26 +0300 Subject: [PATCH 16/30] Updated on 2026-08-14 --- .../hot/DefaultHotMapDerivationsRepository.kt | 9 +++- .../wallets/hot/DefaultHotWalletAccessor.kt | 27 ++++++------ .../hotwallet/accesscode/AccessCodeModel.kt | 41 +++++++------------ gradle/tangem_dependencies.toml | 2 +- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index bdc3f7fdf2..aa09efcfb7 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -7,6 +7,7 @@ import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory import com.tangem.data.wallets.derivations.MissedDerivationsFinder +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -21,6 +22,7 @@ import timber.log.Timber import javax.inject.Inject internal class DefaultHotMapDerivationsRepository @Inject constructor( + private val userWalletsStore: UserWalletsStore, private val networkFactory: NetworkFactory, private val hotWalletAccessor: HotWalletAccessor, private val dispatchers: CoroutineDispatcherProvider, @@ -82,10 +84,15 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( hotWalletId = userWallet.hotWalletId, request = request, ) + + // Get the updated user wallet from the store to ensure we have the latest data + // in case it was modified during the derive operation + val updatedUserWallet = userWalletsStore.getSyncStrict(userWallet.walletId) as UserWallet.Hot + val newKeys = result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) } - return userWallet.updateWithNewKeys(newKeys) to newKeys + return updatedUserWallet.updateWithNewKeys(newKeys) to newKeys } override suspend fun hasMissedDerivations( diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index 00817f74de..f829c76c6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -11,6 +11,7 @@ import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -119,25 +120,27 @@ class DefaultHotWalletAccessor @Inject constructor( originalAuth = auth, auth = auth, block = { blockAuth -> - block(blockAuth).also { - // Update biometry auth if the original auth was password - updateBiometryAuthIfNeeded( - hotWalletId = hotWalletId, - originalAuth = blockAuth, - ) - } + val result = block(blockAuth) + + // Update biometry auth if the original auth was password + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = auth, + ) + + result }, ) } private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { val isAccessCodeRequired = walletsRepository.requireAccessCode() + val isUseBiometricAuthenticationEnabled = walletsRepository.useBiometricAuthentication() - if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + if (originalAuth is HotAuth.Password && isUseBiometricAuthenticationEnabled && isAccessCodeRequired.not()) { val userWallet = userWalletsListRepository.userWalletsSync() - .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } - as? UserWallet.Hot - ?: return + .first { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as UserWallet.Hot val newHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( @@ -161,7 +164,7 @@ class DefaultHotWalletAccessor @Inject constructor( originalAuth: HotAuth, auth: HotAuth, block: suspend (auth: HotAuth) -> T, - ): T = runCatching { + ): T = runSuspendCatching { block(auth) }.getOrElse { exception -> if (auth is HotAuth.Biometry && exception.isBiometryError()) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 318bce1dc7..93c0b75006 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -28,7 +28,6 @@ import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -152,11 +151,6 @@ internal class AccessCodeModel @Inject constructor( tryToAskForBiometry() - userWalletsListRepository.saveWithoutLock( - userWallet.copy(backedUp = true), - canOverride = true, - ) - userWalletsListRepository.setLock( userWallet.walletId, UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), @@ -179,30 +173,25 @@ internal class AccessCodeModel @Inject constructor( hotWalletAccessor.unlockContextual(userWallet.hotWalletId) } - launch(NonCancellable) { - var updatedHotWalletId = tangemHotSdk.changeAuth( + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = unlockHotWallet, - auth = HotAuth.Password(accessCode.toCharArray()), + auth = HotAuth.Biometry, ) - - if (walletsRepository.requireAccessCode().not()) { - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Biometry, - ) - } - - userWalletsListRepository.saveWithoutLock( - userWallet.copy( - hotWalletId = updatedHotWalletId, - backedUp = true, - ), - canOverride = true, - ) - - clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) } + userWalletsListRepository.saveWithoutLock( + userWallet.copy(hotWalletId = updatedHotWalletId), + canOverride = true, + ) + + clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) + params.callbacks.onAccessCodeUpdated(params.userWalletId) } } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ba49247a88..487e084342 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-568" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-531" +tangemHotSdk = "develop-532" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From c9be7fa5c519355c680ad46b09597dcba26b3c02 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 14:57:55 +0300 Subject: [PATCH 17/30] Updated on 2026-08-14 --- .../RoutingTransitionAnimationFactory.kt | 1 - .../wallet/child/wallet/model/WalletModel.kt | 27 +++++ .../model/WalletsUpdateActionResolver.kt | 106 ++++++++++-------- .../domain/WalletAdditionalInfoFactory.kt | 1 + .../state/model/WalletAdditionalInfo.kt | 1 + 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt b/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt index a28e2ff15e..f575967f52 100644 --- a/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt @@ -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))) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 6080809e6d..97c11fb923 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -465,6 +465,7 @@ internal class WalletModel @Inject constructor( when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolver.Action.ReinitializeWallets -> reinitializeWallets(action) is WalletsUpdateActionResolver.Action.AddWallet -> addWallet(action) is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action) is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action) @@ -569,6 +570,32 @@ internal class WalletModel @Inject constructor( ) } + private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) { + action.wallets.forEach { userWallet -> + walletScreenContentLoader.cancel(userWallet.walletId) + tokenListStore.remove(userWallet.walletId) + + walletScreenContentLoader.load( + userWallet = userWallet, + clickIntents = clickIntents, + coroutineScope = modelScope, + ) + + modelScope.launch(dispatchers.main) { + fetchWalletContent(userWallet = userWallet) + } + + stateHolder.update( + ReinitializeWalletTransformer( + prevWalletId = userWallet.walletId, + newUserWallet = userWallet, + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + ), + ) + } + } + private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { if (accountsFeatureToggles.isFeatureEnabled) { fetchWalletContent(userWallet = action.selectedWallet) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 6cce8840ea..82846a7624 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -64,8 +64,11 @@ internal class WalletsUpdateActionResolver @Inject constructor( selectedWallet: UserWallet, ): Action { return when { - isHotWalletUpgraded(state, wallets) -> { - getHotWalletsUpgradedAction(state, wallets) + isAnyHotWalletUpgraded(state, wallets) -> { + getHotWalletsUpgradedAction(state, wallets, selectedWallet) + } + isAnyHotWalletBackedUpChange(state, wallets) -> { + getHotWalletsBackedUpAction(state, wallets) } isWalletsCountChanged(state, wallets) -> { getChangeWalletsListAction(state, wallets, selectedWallet) @@ -79,31 +82,35 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletNameChanged(state, wallets) -> { getRenameWalletsAction(state, wallets) } - isAnyHotWalletBackedUpChange(state, wallets) -> { - getHotWalletsBackedUpAction(state, wallets) + isAnyWalletUnlocked(state, wallets) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) } - else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) + isSelectedWalletCardsCountChanged(state, selectedWallet) -> { + Action.UpdateWalletCardCount(selectedWallet) + } + else -> Action.Unknown } } private fun isAnyHotWalletBackedUpChange(state: WalletScreenState, wallets: List): Boolean { val incompleteActivationWalletIds = state.incompleteActivationWalletIds() - val walletsToUpdate = wallets.filter { - it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId) + return wallets.any { + it is UserWallet.Hot && it.backedUp && incompleteActivationWalletIds.contains(it.walletId) } - return walletsToUpdate.isNotEmpty() } - private fun isHotWalletUpgraded(state: WalletScreenState, wallets: List): Boolean { - val previousWallet = state - .wallets - .getOrNull(state.selectedWalletIndex) - return when (previousWallet) { - is WalletState.MultiCurrency -> { - val wallet = wallets.firstOrNull { it.walletId == previousWallet.walletCardState.id } - previousWallet.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + private fun isAnyHotWalletUpgraded(state: WalletScreenState, wallets: List): Boolean { + return state.wallets.any { walletState -> + when (walletState) { + is WalletState.MultiCurrency -> { + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } + walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + } + else -> false } - else -> false } } @@ -189,9 +196,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( private fun getHotWalletsUpgradedAction( state: WalletScreenState, wallets: List, - ): Action.ReloadWallets { - val walletsToUpdate = wallets.filter { it.walletId == state.getPrevSelectedWallet().id } - return Action.ReloadWallets(walletsToUpdate) + selectedWallet: UserWallet, + ): Action.ReinitializeWallets { + val walletsToUpdate = wallets.filter { wallet -> + val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId } + ?: return@filter false + wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency && + previousState.type == WalletState.MultiCurrency.WalletType.Hot + } + return Action.ReinitializeWallets(selectedWallet, walletsToUpdate) } private fun getRenameWalletsAction(state: WalletScreenState, wallets: List): Action.RenameWallets { @@ -205,29 +218,14 @@ internal class WalletsUpdateActionResolver @Inject constructor( ) } - private fun getUpdateSelectedWalletAction( - state: WalletScreenState, - wallets: List, - selectedWallet: UserWallet, - ): Action { - return when { - isSelectedWalletUnlocked(state, selectedWallet) -> { - Action.UnlockWallet( - selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), - ) - } - isSelectedWalletCardsCountChanged(state, selectedWallet) -> { - Action.UpdateWalletCardCount(selectedWallet) - } - else -> Action.Unknown + private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List): Boolean { + return state.wallets.any { walletState -> + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false + !wallet.isLocked && + (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) } } - private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean { - return state.isSelectedWalletLocked() && !selectedWallet.isLocked - } - private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { if (selectedWallet !is UserWallet.Cold) return false val prevSelectedWallet = state.getPrevSelectedWallet() @@ -235,12 +233,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( prevSelectedWallet.cardCount != selectedWallet.getCardsCount() } - private fun WalletScreenState.isSelectedWalletLocked(): Boolean { - val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found") - return selectedWalletState is WalletState.MultiCurrency.Locked || - selectedWalletState is WalletState.SingleCurrency.Locked - } - private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState { return wallets .map(WalletState::walletCardState) @@ -249,9 +241,12 @@ internal class WalletsUpdateActionResolver @Inject constructor( } private fun WalletScreenState.incompleteActivationWalletIds(): List { - return wallets.mapNotNull { - if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { - it.walletCardState.id + return wallets.mapNotNull { wallet -> + if (wallet.warnings.any { it is WalletNotification.FinishWalletActivation } || + wallet.walletCardState is WalletState.MultiCurrency && + wallet.walletCardState.additionalInfo?.isHotBackedUp == false + ) { + wallet.walletCardState.id } else { null } @@ -306,6 +301,19 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } + /** + * Reinitialize wallets + */ + data class ReinitializeWallets( + val selectedWallet: UserWallet, + val wallets: List, + ) : Action() { + + override fun toString(): String { + return "ReinitializeWallets(wallets = ${wallets.joinToString { it.walletId.toString() }}" + } + } + /** * Rename wallets * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 31fffb5d04..b911a9a509 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -50,6 +50,7 @@ internal object WalletAdditionalInfoFactory { backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) else -> TextReference.Str("") }, + isHotBackedUp = backedUp, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt index 2e210c2221..a12cd21993 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt @@ -7,4 +7,5 @@ import com.tangem.core.ui.extensions.TextReference data class WalletAdditionalInfo( val hideable: Boolean, val content: TextReference, + val isHotBackedUp: Boolean = false, ) \ No newline at end of file From d47433ee6cd1faefc6ad31b4676f20ad9efc4728 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 13:23:56 +0400 Subject: [PATCH 18/30] Updated on 2026-08-14 --- data/account/detekt-baseline-debug.xml | 2 - .../AccountListCryptoCurrenciesProducer.kt | 8 ++- ...aultMultiWalletCryptoCurrenciesProducer.kt | 4 +- ...MultiWalletCryptoCurrenciesProducerTest.kt | 15 ++++-- .../DefaultCardCryptoCurrencyFactory.kt | 12 ++++- .../ResponseCryptoCurrenciesFactory.kt | 10 ++-- data/manage-tokens/detekt-baseline-debug.xml | 8 --- .../DefaultManageTokensRepository.kt | 33 ++++++++----- .../utils/ManagedCryptoCurrencyFactory.kt | 49 +++++++++++++------ data/swap/build.gradle.kts | 1 + .../SavedSwapTransactionConverter.kt | 18 +++++++ .../SavedSwapTransactionListConverter.kt | 6 ++- data/tokens/detekt-baseline-debug.xml | 3 -- .../repository/DefaultCurrenciesRepository.kt | 19 ++++--- data/wallets/detekt-baseline-debug.xml | 4 -- .../DefaultDerivationsRepository.kt | 24 +++++---- .../hot/DefaultHotMapDerivationsRepository.kt | 9 ++-- detekt_baseline_report.txt | 18 +++---- domain/markets/detekt-baseline-debug.xml | 1 - .../domain/markets/SaveMarketTokensUseCase.kt | 11 +++-- .../derivations/DerivationsRepository.kt | 7 ++- .../HotMapDerivationsRepository.kt | 2 + features/swap/data/build.gradle.kts | 4 +- .../SavedSwapTransactionListConverter.kt | 15 ++++++ 24 files changed, 193 insertions(+), 90 deletions(-) diff --git a/data/account/detekt-baseline-debug.xml b/data/account/detekt-baseline-debug.xml index f0b34d22a4..014cb3a866 100644 --- a/data/account/detekt-baseline-debug.xml +++ b/data/account/detekt-baseline-debug.xml @@ -6,8 +6,6 @@ MultilineLambdaItParameter:DefaultAccountsCRUDRepository.kt$DefaultAccountsCRUDRepository${ if (it is HttpException && it.code == HttpException.Code.NOT_MODIFIED) { null } else { throw it } } MultilineLambdaItParameter:GetWalletAccountsResponseExt.kt${ enrichedTokensByAccountId[it].orEmpty().map { token -> // Tokens from unexisting accounts should be copied to the main account token.copy(accountId = accountDTO.id) } } NoNameShadowing:GetWalletAccountsResponseExt.kt$tokens - NullableToStringCall:AccountListCryptoCurrenciesProducer.kt$AccountListCryptoCurrenciesProducer$${this::class.simpleName} - NullableToStringCall:DefaultMultiWalletCryptoCurrenciesProducer.kt$DefaultMultiWalletCryptoCurrenciesProducer$${this::class.simpleName} UnnecessaryLet:DefaultAccountsCRUDRepository.kt$DefaultAccountsCRUDRepository$let(AccountName::invoke) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt index 9722001f29..4cd149ae75 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt @@ -36,11 +36,12 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( override val fallback: Option> = emptySet().some() + @Suppress("NullableToStringCall") override fun produce(): Flow> { 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, ) } } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt index 8c46088747..25255d60b4 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -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()) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt index 983ba0706e..c622f073a4 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -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()) } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index 626f998bfb..2d7a8855d7 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -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 } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 66faea0a74..eccf6e4952 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -22,7 +22,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( fun createCurrencies( response: UserTokensResponse, userWallet: UserWallet, - accountIndex: DerivationIndex? = null, + accountIndex: DerivationIndex, ): List { return createCurrencies(tokens = response.tokens, userWallet = userWallet, accountIndex = accountIndex) } @@ -30,7 +30,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( fun createCurrencies( tokens: List, userWallet: UserWallet, - accountIndex: DerivationIndex? = null, + accountIndex: DerivationIndex, ): List { 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( diff --git a/data/manage-tokens/detekt-baseline-debug.xml b/data/manage-tokens/detekt-baseline-debug.xml index d384bf15ce..a9b6d430bb 100644 --- a/data/manage-tokens/detekt-baseline-debug.xml +++ b/data/manage-tokens/detekt-baseline-debug.xml @@ -3,15 +3,7 @@ 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, ) } - MultilineLambdaItParameter:DefaultManageTokensRepository.kt$DefaultManageTokensRepository${ it.contractAddress != null && it.networkId == network.backendId && it.derivationPath == network.derivationPath.value } MultilineLambdaItParameter:ManageTokensUpdateFetcher.kt$ManageTokensUpdateFetcher${ if (it.key == toUpdate[index].key) { Batch(it.key, updatedItems) } else { null } } - NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$create(coinsResponse, tokensResponse, userWallet, accountIndex) - NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet, accountIndex) - NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$findAddedInNetworks(coinResponse.id, tokensResponse, userWallet, accountIndex) - NamedArguments:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$findAddedInNetworks(testnetToken.id, tokensResponse, userWallet, accountIndex) - SuspendFunSwallowedCancellation:DefaultManageTokensRepository.kt$DefaultManageTokensRepository$runCatching UnsafeCallOnNullableType:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository$coinNetwork.decimalCount!! - UseOrEmpty:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$testnetToken.networks?.mapNotNull { network -> createSource( networkId = network.id, contractAddress = network.address, decimals = network.decimalCount, userWallet = userWallet, accountIndex = accountIndex, ) } ?: emptyList() - UseOrEmpty:ManagedCryptoCurrencyFactory.kt$ManagedCryptoCurrencyFactory$tokensResponse ?.let { createCustomTokens(it, userWallet, accountIndex) } ?: emptyList() diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index ed2887e114..84eea67a90 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -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 } } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 5ecaec838c..49ec7c78de 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -35,10 +35,16 @@ internal class ManagedCryptoCurrencyFactory( coinsResponse: CoinsResponse, tokensResponse: UserTokensResponse?, userWallet: UserWallet?, - accountIndex: DerivationIndex?, + accountIndex: DerivationIndex, ): List { 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 { 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 { 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 = 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 { if (tokensResponse == null) return emptySet() diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts index 78b5b851c7..a7ea5bd55c 100644 --- a/data/swap/build.gradle.kts +++ b/data/swap/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { /** Libs */ implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) /** Other */ implementation(deps.androidx.datastore) diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt index c07926cd50..8895c05ffd 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt @@ -1,12 +1,16 @@ package com.tangem.data.swap.converter.transaction +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.swap.models.SwapStatusDTO import com.tangem.data.swap.models.SwapTransactionDTO import com.tangem.data.swap.models.SwapTxTypeDTO +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapTransactionModel import com.tangem.domain.swap.models.SwapTxType +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import com.tangem.utils.converter.TwoWayConverter internal class SavedSwapTransactionConverter( @@ -48,9 +52,23 @@ internal class SavedSwapTransactionConverter( ): SwapTransactionModel { val status = txStatuses[value.txId] val refundCurrency = status?.refundTokensResponse?.let { id -> + val blockchain = Blockchain.fromNetworkId(id.networkId) ?: return@let null + val derivationPath = id.derivationPath ?: return@let null + + val accountIndex = if (blockchain == Blockchain.Chia) { + DerivationIndex.Main + } else { + val recognizer = AccountNodeRecognizer(blockchain = blockchain) + val index = recognizer.recognize(derivationPathValue = derivationPath)?.toInt() + ?: return@let null + + DerivationIndex(index).getOrNull() ?: return@let null + } + responseCryptoCurrenciesFactory.createCurrency( responseToken = id, userWallet = userWallet, + accountIndex = accountIndex, ) } val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt index 9688b3086b..051b7d9a47 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -72,7 +72,11 @@ internal class SavedSwapTransactionListConverter( return SwapTransactionListModel( transactions = value.transactions.map { tx -> - savedSwapTransactionConverter.convertBack(tx, userWallet, txStatuses) + savedSwapTransactionConverter.convertBack( + value = tx, + userWallet = userWallet, + txStatuses = txStatuses, + ) }, userWalletId = value.userWalletId, fromCryptoCurrencyId = value.fromCryptoCurrencyId, diff --git a/data/tokens/detekt-baseline-debug.xml b/data/tokens/detekt-baseline-debug.xml index 8be9ea8bd3..bc443d0652 100644 --- a/data/tokens/detekt-baseline-debug.xml +++ b/data/tokens/detekt-baseline-debug.xml @@ -3,11 +3,8 @@ MultilineLambdaItParameter:CustomTokensMerger.kt$CustomTokensMerger${ Timber.e(it, "Unable to fetch token:\n$token") null } - MultilineLambdaItParameter:DefaultCurrenciesRepository.kt$DefaultCurrenciesRepository${ it.networkId == blockchainNetworkId && compareIdWithMigrations(it, coinId) && it.derivationPath == derivationPath.value } NullableToStringCall:AccountListCryptoCurrenciesFetcher.kt$AccountListCryptoCurrenciesFetcher$${this::class.simpleName} NullableToStringCall:DefaultMultiWalletCryptoCurrenciesFetcher.kt$DefaultMultiWalletCryptoCurrenciesFetcher$${this::class.simpleName} - SuspendFunSwallowedCancellation:DefaultCurrenciesRepository.kt$DefaultCurrenciesRepository$runCatching - SuspendFunWithFlowReturnType:DefaultCurrenciesRepository.kt$DefaultCurrenciesRepository$suspend UseOrEmpty:DefaultYieldSupplyWarningsViewedRepository.kt$DefaultYieldSupplyWarningsViewedRepository$appPreferencesStore.getObjectSet<String>(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull() ?: emptySet() diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index e2988f92ce..ffb98afb6c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -21,6 +21,7 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -31,6 +32,7 @@ import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -301,8 +303,9 @@ internal class DefaultCurrenciesRepository( ) responseCryptoCurrenciesFactory.createCurrencies( - storedTokens, + response = storedTokens, userWallet = userWallet, + accountIndex = DerivationIndex.Main, ) } @@ -357,15 +360,16 @@ internal class DefaultCurrenciesRepository( val coinId = blockchain.toCoinId() val storedCoin = storedTokens.tokens - .find { - it.networkId == blockchainNetworkId && - compareIdWithMigrations(it, coinId) && - it.derivationPath == derivationPath.value + .find { token -> + token.networkId == blockchainNetworkId && + compareIdWithMigrations(token, coinId) && + token.derivationPath == derivationPath.value } ?: error("Coin in this network $networkId not found") val coin = responseCryptoCurrenciesFactory.createCurrency( responseToken = storedCoin, userWallet = userWallet, + accountIndex = DerivationIndex.Main, ) coin as? CryptoCurrency.Coin ?: error("Unable to create currency") @@ -525,6 +529,7 @@ internal class DefaultCurrenciesRepository( } } + @Suppress("SuspendFunWithFlowReturnType") private suspend fun getCurrenciesForWallet( userWallet: UserWallet, currencyRawId: CryptoCurrency.RawID, @@ -539,6 +544,7 @@ internal class DefaultCurrenciesRepository( responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens.copy(tokens = filterResponse), userWallet = userWallet, + accountIndex = DerivationIndex.Main, ) } } @@ -570,7 +576,7 @@ internal class DefaultCurrenciesRepository( } override suspend fun syncTokens(userWalletId: UserWalletId) { - runCatching { + runSuspendCatching { val savedCurrencies = requireNotNull( value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, @@ -591,6 +597,7 @@ internal class DefaultCurrenciesRepository( responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens, userWallet = userWallet, + accountIndex = DerivationIndex.Main, ) } } diff --git a/data/wallets/detekt-baseline-debug.xml b/data/wallets/detekt-baseline-debug.xml index 26f5591932..7ac40bc693 100644 --- a/data/wallets/detekt-baseline-debug.xml +++ b/data/wallets/detekt-baseline-debug.xml @@ -3,8 +3,6 @@ MultilineLambdaItParameter:DefaultColdMapDerivationsRepository.kt$DefaultColdMapDerivationsRepository${ networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, extraDerivationPath = null, userWallet = userWallet, ) } - MultilineLambdaItParameter:DefaultDerivationsRepository.kt$DefaultDerivationsRepository${ userWallet.update(it.first) it.second } - MultilineLambdaItParameter:DefaultHotMapDerivationsRepository.kt$DefaultHotMapDerivationsRepository${ networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, extraDerivationPath = null, userWallet = userWallet, ) } MultilineLambdaItParameter:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository${ AttemptsPersistentData( attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0, bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0, deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L, ) } MultilineLambdaItParameter:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository${ it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) } MultilineLambdaItParameter:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository${ while (true) { emit(toState(id, it.attempts, it.deadline, it.bootCount)) val remaining = remainingSeconds(it.deadline, it.bootCount) if (remaining <= 0) break delay(timeMillis = 1000) } } @@ -16,9 +14,7 @@ SuspendFunSwallowedCancellation:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor$runCatching SuspendFunSwallowedCancellation:TangemHotWalletSigner.kt$TangemHotWalletSigner$runCatching UnnecessaryLet:MissedDerivationsFinder.kt$MissedDerivationsFinder$let(::findByNetworks) - UnusedImports:DefaultDerivationsRepository.kt$import com.tangem.common.map UseOrEmpty:DefaultColdMapDerivationsRepository.kt$DefaultColdMapDerivationsRepository$oldKeys[walletKey] ?: emptyMap() - UseOrEmpty:DefaultHotMapDerivationsRepository.kt$DefaultHotMapDerivationsRepository$oldKeys[walletKey] ?: emptyMap() VarCouldBeVal:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor$private var contextualUnlockHotWallet: ConcurrentHashMap<HotWalletId, UnlockHotWallet?> = ConcurrentHashMap() diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt index 862b90b035..3a3dcf27ac 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -2,16 +2,16 @@ package com.tangem.data.wallets.derivations import com.tangem.common.CompletionResult import com.tangem.common.extensions.ByteArrayKey -import com.tangem.common.map import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository -import com.tangem.domain.wallets.derivations.DerivationsRepository -import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -29,11 +29,17 @@ internal class DefaultDerivationsRepository @Inject constructor( derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) } - override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { + override suspend fun derivePublicKeysByNetworkIds( + userWalletId: UserWalletId, + networkIds: List, + accountIndex: DerivationIndex, + ) { val userWallet = userWalletsStore.getSyncStrict(userWalletId) when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) - is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) + is UserWallet.Hot -> { + hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds, accountIndex) + } }.also { userWallet.update(it) } @@ -57,9 +63,9 @@ internal class DefaultDerivationsRepository @Inject constructor( return when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations) is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations) - }.let { - userWallet.update(it.first) - it.second + }.let { publicKeysMapByUserWallet -> + userWallet.update(publicKeysMapByUserWallet.first) + publicKeysMapByUserWallet.second } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index aa09efcfb7..987dd65cc5 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -8,6 +8,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory import com.tangem.data.wallets.derivations.MissedDerivationsFinder 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.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -38,14 +39,16 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( override suspend fun derivePublicKeysByNetworkIds( userWallet: UserWallet.Hot, networkIds: List, + accountIndex: DerivationIndex, ): UserWallet.Hot { return derivePublicKeysByNetworks( userWallet = userWallet, - networks = networkIds.mapNotNull { + networks = networkIds.mapNotNull { networkRawId -> networkFactory.create( - blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, + blockchain = Blockchain.fromNetworkId(networkRawId.value) ?: return@mapNotNull null, extraDerivationPath = null, userWallet = userWallet, + accountIndex = accountIndex, ) }, ) @@ -138,7 +141,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( ): Map { return (oldKeys.keys + newKeys.keys).toSet() .associateWith { walletKey -> - val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap()) + val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey].orEmpty()) val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) ExtendedPublicKeysMap(oldDerivations + newDerivations) diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 6e74d6e47d..85ca6a00e5 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-01 16:30:16 +Date: 2025-12-02 13:19:49 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1453 + Total Issues: 1435 Modules with Issues: 62 Average Issues per Module: 23 Progress: - Fixed: 349 out of 1802 (19%) - Remaining: 1453 + Fixed: 367 out of 1802 (20%) + Remaining: 1435 ========================================== All Modules with Issues (sorted by count) @@ -54,14 +54,13 @@ data/visa 22 features/yield-supply/impl 21 features/tangempay/details/impl 21 data/nft 20 -data/wallets 18 features/swap/data 15 +data/wallets 14 data/swap 13 features/token-recieve/impl 11 features/qr-scanning/impl 11 domain/account/status 11 data/onramp 11 -data/manage-tokens 11 core/datasource 11 data/markets 10 features/details/impl 9 @@ -72,22 +71,23 @@ features/referral/impl 8 domain/transaction 8 libs/tangem-sdk-api 7 data/txhistory 7 -data/tokens 7 -data/account 7 features/welcome/impl 6 -domain/markets 6 data/wallet-manager 6 libs/visa 5 features/send-v2/api 5 features/home/impl 5 +domain/markets 5 domain/legacy 5 data/transaction 5 +data/account 5 features/referral/domain 4 features/biometry/impl 4 +data/tokens 4 features/txhistory/impl 3 features/tangempay/onboarding/impl 3 features/create-wallet-start/impl 3 domain/manage-tokens 3 +data/manage-tokens 3 common/routing 3 features/account/api 2 data/promo 2 diff --git a/domain/markets/detekt-baseline-debug.xml b/domain/markets/detekt-baseline-debug.xml index ef5fed0f16..64d62630c1 100644 --- a/domain/markets/detekt-baseline-debug.xml +++ b/domain/markets/detekt-baseline-debug.xml @@ -5,7 +5,6 @@ BooleanPropertyNaming:FilterAvailableNetworksForWalletUseCase.kt$FilterAvailableNetworksForWalletUseCase$private val useNewRepository: Boolean BooleanPropertyNaming:GetStakingNotificationMaxApyUseCase.kt$GetStakingNotificationMaxApyUseCase$val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) { currentDate - walletFirstUsageDate > TWO_WEEKS_IN_MILLIS } else { false } MultilineLambdaItParameter:FilterAvailableNetworksForWalletUseCase.kt$FilterAvailableNetworksForWalletUseCase${ val blockchain = Blockchain.fromNetworkId(it.networkId) supportedBlockchains.contains(blockchain) } - MultilineLambdaItParameter:SaveMarketTokensUseCase.kt$SaveMarketTokensUseCase${ marketsTokenRepository.createCryptoCurrency( userWalletId = userWalletId, token = tokenMarketParams, network = it, ) } SuspendFunWithFlowReturnType:GetStakingNotificationMaxApyUseCase.kt$GetStakingNotificationMaxApyUseCase$suspend SuspendFunWithFlowReturnType:MarketsTokenRepository.kt$MarketsTokenRepository$suspend diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index 77e9666e1e..c1d1b50552 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -45,11 +46,11 @@ class SaveMarketTokensUseCase( removedNetworks: Set, ): Either = Either.catch { if (removedNetworks.isNotEmpty()) { - val removedCurrencies = removedNetworks.mapNotNull { + val removedCurrencies = removedNetworks.mapNotNull { network -> marketsTokenRepository.createCryptoCurrency( userWalletId = userWalletId, token = tokenMarketParams, - network = it, + network = network, ) } @@ -60,13 +61,15 @@ class SaveMarketTokensUseCase( derivationsRepository.derivePublicKeysByNetworkIds( userWalletId = userWalletId, networkIds = addedNetworks.map { Network.RawID(it.networkId) }, + accountIndex = DerivationIndex.Main, ) - val addedCurrencies = addedNetworks.mapNotNull { + val addedCurrencies = addedNetworks.mapNotNull { network -> marketsTokenRepository.createCryptoCurrency( userWalletId = userWalletId, token = tokenMarketParams, - network = it, + network = network, + accountIndex = DerivationIndex.Main, ) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt index 43b5190947..9eefdc6899 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.wallets.derivations import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -13,7 +14,11 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) - suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) + suspend fun derivePublicKeysByNetworkIds( + userWalletId: UserWalletId, + networkIds: List, + accountIndex: DerivationIndex, + ) @Throws suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt index 65364e9a80..27b260db1d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.wallets.derivations import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -16,6 +17,7 @@ interface HotMapDerivationsRepository { suspend fun derivePublicKeysByNetworkIds( userWallet: UserWallet.Hot, networkIds: List, + accountIndex: DerivationIndex, ): UserWallet.Hot @Throws diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 046da4aa6a..4efc95da23 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) @@ -45,6 +44,9 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account.status) + implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) + /** Data */ implementation(projects.data.common) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 3144aa0a95..8ec8958bd3 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -17,6 +17,7 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModelInner import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import com.tangem.utils.converter.Converter internal class SavedSwapTransactionListConverter( @@ -75,9 +76,23 @@ internal class SavedSwapTransactionListConverter( .map { tx -> val status = txStatuses[tx.txId] val refundCurrency = status?.refundTokensResponse?.let { id -> + val blockchain = Blockchain.fromNetworkId(id.networkId) ?: return@let null + val derivationPath = id.derivationPath ?: return@let null + + val accountIndex = if (blockchain == Blockchain.Chia) { + DerivationIndex.Main + } else { + val recognizer = AccountNodeRecognizer(blockchain = blockchain) + val index = recognizer.recognize(derivationPathValue = derivationPath)?.toInt() + ?: return@let null + + DerivationIndex(index).getOrNull() ?: return@let null + } + responseCryptoCurrenciesFactory.createCurrency( responseToken = id, userWallet = userWallet, + accountIndex = accountIndex, ) } val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) From ccb8c774ed84c3d4caaea044d0f7d4183486408f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Dec 2025 19:35:04 +0500 Subject: [PATCH 19/30] Updated on 2026-08-14 --- .../intents/WalletWarningsClickIntents.kt | 43 ++---------- .../common/preview/WalletScreenPreviewData.kt | 4 +- .../utils/WalletWarningsSingleEventSender.kt | 65 ++++++++++++++++++- .../domain/GetMultiWalletWarningsFactory.kt | 12 +--- .../wallet/state/model/WalletNotification.kt | 8 ++- 5 files changed, 78 insertions(+), 54 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index e4f32d06b9..1fdec44c36 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -11,9 +11,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -40,7 +38,6 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBan import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.usecase.* -import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic @@ -106,7 +103,7 @@ internal interface WalletWarningsClickIntents { fun onDenyPermissions() - fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) + fun onFinishWalletActivationClick(isBackupExists: Boolean) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -138,7 +135,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, private val notificationsRepository: NotificationsRepository, - private val messageSender: UiMessageSender, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, @@ -461,40 +457,9 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) { - when (bannerType) { - WalletActivationBannerType.Attention -> { - val userWallet = getSelectedUserWallet() ?: return - appRouter.push(WalletActivation(userWallet.walletId, isBackupExists)) - } - WalletActivationBannerType.Warning -> { - val message = bottomSheetMessage { - infoBlock { - icon(R.drawable.img_knight_shield_32) { - type = MessageBottomSheetUMV2.Icon.Type.Warning - backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint - } - title = resourceReference(R.string.hw_activation_need_title) - body = resourceReference(R.string.hw_activation_need_description) - } - secondaryButton { - text = resourceReference(R.string.common_later) - onClick { - closeBs() - } - } - primaryButton { - text = resourceReference(R.string.hw_activation_need_backup) - onClick { - val userWallet = getSelectedUserWallet() ?: return@onClick - appRouter.push(WalletActivation(userWallet.walletId, isBackupExists)) - closeBs() - } - } - } - messageSender.send(message) - } - } + override fun onFinishWalletActivationClick(isBackupExists: Boolean) { + val userWalletId = stateHolder.getSelectedWalletId() + appRouter.push(WalletActivation(userWalletId, isBackupExists)) } override fun onAllowPermissions() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index abedb142a7..4e0c498af1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.AccountItemPreviewData import com.tangem.core.ui.components.token.state.TokenItemState @@ -14,6 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -167,7 +167,7 @@ internal object WalletScreenPreviewData { warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, WalletNotification.FinishWalletActivation( - iconTint = NotificationConfig.IconTint.Attention, + type = WalletActivationBannerType.Attention, buttonsState = ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), onClick = { }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index 7bf7785b0a..0813bc4d40 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -1,8 +1,24 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils +import arrow.atomic.AtomicBoolean +import com.tangem.common.routing.AppRoute.WalletActivation import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.primaryButton +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -12,8 +28,13 @@ import javax.inject.Inject internal class WalletWarningsSingleEventSender @Inject constructor( private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val screenLifecycleProvider: ScreenLifecycleProvider, + private val uiMessageSender: UiMessageSender, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val router: Router, ) { + private val isActivationBottomSheetShown: AtomicBoolean = AtomicBoolean(false) + suspend fun send( userWalletId: UserWalletId, displayedUiState: WalletState?, @@ -26,9 +47,49 @@ internal class WalletWarningsSingleEventSender @Inject constructor( val events = newWarnings.filter { it !in displayedUiState.warnings } events.forEach { event -> - if (event is WalletNotification.Critical.SeedPhraseNotification) { - seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + when (event) { + is WalletNotification.Critical.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotification.FinishWalletActivation -> { + if (event.type == WalletActivationBannerType.Warning && !isActivationBottomSheetShown.get()) { + showFinishActivationBottomSheet(userWalletId) + isActivationBottomSheetShown.set(true) + } + } + else -> Unit } } } + + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return + if (userWallet !is UserWallet.Hot) return + + val message = bottomSheetMessage { + infoBlock { + icon(R.drawable.img_knight_shield_32) { + type = MessageBottomSheetUMV2.Icon.Type.Warning + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_activation_need_title) + body = resourceReference(R.string.hw_activation_need_description) + } + secondaryButton { + text = resourceReference(R.string.common_later) + onClick { + closeBs() + } + } + primaryButton { + text = resourceReference(R.string.hw_activation_need_backup) + onClick { + router.push(WalletActivation(userWallet.walletId, userWallet.backedUp)) + closeBs() + } + } + } + + uiMessageSender.send(message) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index c2db89f8c8..457df41e5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -7,7 +7,6 @@ import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState -import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver @@ -465,22 +464,17 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ifError = { WalletActivationBannerType.Attention }, ) - val tint = when (type) { - WalletActivationBannerType.Attention -> IconTint.Attention - WalletActivationBannerType.Warning -> IconTint.Warning - } - addIf( element = WalletNotification.FinishWalletActivation( - iconTint = tint, + type = type, buttonsState = when (type) { WalletActivationBannerType.Warning -> ButtonsState.PrimaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), - onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) }, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, ) else -> ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), - onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) }, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, ) }, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 4553d72fc9..0ddf1d2139 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -287,14 +288,17 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class FinishWalletActivation( - val iconTint: IconTint, + val type: WalletActivationBannerType, val buttonsState: ButtonsState, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), subtitle = resourceReference(R.string.hw_activation_need_description), iconResId = R.drawable.img_knight_shield_32, - iconTint = iconTint, + iconTint = when (type) { + WalletActivationBannerType.Attention -> IconTint.Attention + WalletActivationBannerType.Warning -> IconTint.Warning + }, buttonsState = buttonsState, ), ) From 57d3a102abdadfcf0fef2f5806e807ee19cffe58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Dec 2025 14:25:18 +0300 Subject: [PATCH 20/30] Updated on 2026-08-14 --- .../data/wallets/DefaultWalletsRepository.kt | 19 ++++++++++++++----- .../wallets/repository/WalletsRepository.kt | 7 +++++-- .../wallets/usecase/RenameWalletUseCase.kt | 2 +- .../model/MultiWalletFinalizeModel.kt | 10 ++++++++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index ba985743b4..982812c9b8 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -344,18 +344,27 @@ internal class DefaultWalletsRepository( upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = UserWalletId(walletId)) + override suspend fun setWalletName(walletId: UserWalletId, walletName: String) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(key = walletId) tangemTechApi.updateWallet( - walletId = walletId, + walletId = walletId.stringValue, body = WalletBody(name = walletName, type = WalletType.from(userWallet)), ).getOrThrow() } - override suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo = withContext(dispatchers.io) { + override suspend fun upgradeWallet(walletId: UserWalletId) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncStrict(key = walletId) + + tangemTechApi.updateWallet( + walletId = walletId.stringValue, + body = WalletBody(name = userWallet.name, type = WalletType.from(userWallet)), + ).getOrThrow() + } + + override suspend fun getWalletInfo(walletId: UserWalletId): UserWalletRemoteInfo = withContext(dispatchers.io) { UserWalletRemoteInfoConverter.convert( - value = tangemTechApi.getWalletById(walletId).getOrThrow(), + value = tangemTechApi.getWalletById(walletId.stringValue).getOrThrow(), ) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 2cbdf16c5c..96266b809d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -64,10 +64,13 @@ interface WalletsRepository { suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) @Throws - suspend fun setWalletName(walletId: String, walletName: String) + suspend fun setWalletName(walletId: UserWalletId, walletName: String) @Throws - suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo + suspend fun upgradeWallet(walletId: UserWalletId) + + @Throws + suspend fun getWalletInfo(walletId: UserWalletId): UserWalletRemoteInfo @Throws suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index b69d760d71..2f5ffa9dde 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -16,7 +16,7 @@ class RenameWalletUseCase( suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = either { runCatching { - walletsRepository.setWalletName(userWalletId.stringValue, name) + walletsRepository.setWalletName(userWalletId, name) } userWalletsSyncDelegate.syncWallet(userWalletId, name).bind() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 7664244a73..4c9424cf98 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -41,6 +41,7 @@ import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.StringsSigns import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -91,7 +92,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( // sets proper artwork state for initial step // (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY]) when (getInitialStep()) { - MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ } + MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ + } MultiWalletFinalizeUM.Step.BackupDevice1 -> { onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) } @@ -299,7 +301,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( .updateWithHotWallet(wallet), ) }, - ).getOrElse { + ).onRight { + launch(NonCancellable) { + runSuspendCatching { walletsRepository.upgradeWallet(userWalletCreated.walletId) } + } + }.getOrElse { error("Failed to upgrade to cold wallet. Error: $it") } } From c501b560630af0d91203b46277a42045b05716c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Dec 2025 16:00:38 +0400 Subject: [PATCH 21/30] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 3 + .../tap/di/domain/TokensDomainModule.kt | 7 +- data/common/build.gradle.kts | 1 + .../UserTokensResponseAddressesEnricher.kt | 52 ++---- .../tangem/data/common/di/DataCommonModule.kt | 6 +- ...UserTokensResponseAddressesEnricherTest.kt | 173 +++++++----------- data/wallet-manager/detekt-baseline-debug.xml | 3 - .../DefaultWalletManagersFacade.kt | 76 ++++---- data/wallets/detekt-baseline-debug.xml | 1 - detekt_baseline_report.txt | 22 +-- domain/account/status/build.gradle.kts | 1 + .../account/status/detekt-baseline-debug.xml | 2 - .../status/di/AccountStatusUseCaseModule.kt | 5 +- .../usecase/ManageCryptoCurrenciesUseCase.kt | 66 +++++-- .../utils/CryptoCurrencyBalanceFetcher.kt | 17 +- domain/manage-tokens/build.gradle.kts | 2 + .../managetokens/SaveManagedTokensUseCase.kt | 24 ++- domain/markets/build.gradle.kts | 1 + .../domain/markets/SaveMarketTokensUseCase.kt | 25 ++- domain/tokens/detekt-baseline-debug.xml | 2 - .../tokens/AddCryptoCurrenciesUseCase.kt | 60 ++++-- .../tester/impl/detekt-baseline-debug.xml | 1 - .../wallet/impl/detekt-baseline-debug.xml | 2 - 23 files changed, 296 insertions(+), 256 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 9b33e83a66..c3b9c168e1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -12,6 +12,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher 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,6 +66,7 @@ object MarketsDomainModule { fun provideSaveMarketTokensUseCase( derivationsRepository: DerivationsRepository, marketsTokenRepository: MarketsTokenRepository, + walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, @@ -75,6 +77,7 @@ object MarketsDomainModule { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, marketsTokenRepository = marketsTokenRepository, + walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c9c0d08868..02b87eb3fa 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -20,7 +20,10 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier 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,6 +43,7 @@ internal object TokensDomainModule { @Singleton fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, + walletManagersFacade: WalletManagersFacade, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -48,6 +52,7 @@ internal object TokensDomainModule { ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, + walletManagersFacade = walletManagersFacade, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index b418e04abc..cb2baa1194 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -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 */ diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt index aff3f5c5fa..dfd790c608 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt @@ -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 - } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index fb49316219..48751fa834 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -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, ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt index 77b27593e6..7e9f617946 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt @@ -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 { + val wallet = mockk { + 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 { + val wallet = mockk { + 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 { - 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) diff --git a/data/wallet-manager/detekt-baseline-debug.xml b/data/wallet-manager/detekt-baseline-debug.xml index b5ce5d471d..967025b8b6 100644 --- a/data/wallet-manager/detekt-baseline-debug.xml +++ b/data/wallet-manager/detekt-baseline-debug.xml @@ -2,10 +2,7 @@ - MultilineLambdaItParameter:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade${ Token( name = it.name, symbol = it.symbol, contractAddress = it.contractAddress, decimals = it.decimals, id = it.id, ) } MultilineLambdaItParameter:UpdateWalletManagerResultFactory.kt$UpdateWalletManagerResultFactory${ createCurrencyTransaction( txHistoryItemConverter = txHistoryItemConverter, data = it, ) } - NamedArguments:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) - UnnecessaryLet:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$let(txHistoryStateConverter::convert) UnsafeCallOnNullableType:WalletManagerFactory.kt$blockchain.getTestnetVersion()!! UnsafeCallOnNullableType:WalletManagerFactory.kt$scanResponse.secondTwinPublicKey!! diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 5b0f50b11d..7e669ccebb 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -85,7 +85,12 @@ internal class DefaultWalletManagersFacade @Inject constructor( val blockchain = network.toBlockchain() val derivationPath = network.derivationPath.value - return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) + return getAndUpdateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = derivationPath, + extraTokens = extraTokens, + ) } override suspend fun remove(userWalletId: UserWalletId, networks: Set) { @@ -123,18 +128,18 @@ internal class DefaultWalletManagersFacade @Inject constructor( if (tokenInfos.isEmpty()) return tokenInfos - .groupBy { it.network } + .groupBy(TokenInfo::network) .forEach { (network, tokenInfoList) -> removeTokens( userWalletId = userWalletId, network = network, - networkTokens = tokenInfoList.map { + networkTokens = tokenInfoList.map { tokenInfo -> Token( - name = it.name, - symbol = it.symbol, - contractAddress = it.contractAddress, - decimals = it.decimals, - id = it.id, + name = tokenInfo.name, + symbol = tokenInfo.symbol, + contractAddress = tokenInfo.contractAddress, + decimals = tokenInfo.decimals, + id = tokenInfo.id, ) }, ) @@ -215,24 +220,24 @@ internal class DefaultWalletManagersFacade @Inject constructor( "Unable to get a wallet manager for blockchain: ${currency.network}" } - return walletManager - .getTransactionHistoryState( - address = walletManager.wallet.address, - filterType = when (currency) { - is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> { - val blockchainToken = Token( - name = currency.name, - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - id = currency.id.rawCurrencyId?.value, - ) - TransactionHistoryRequest.FilterType.Contract(blockchainToken) - } - }, - ) - .let(txHistoryStateConverter::convert) + val transactionHistoryState = walletManager.getTransactionHistoryState( + address = walletManager.wallet.address, + filterType = when (currency) { + is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId?.value, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } + }, + ) + + return txHistoryStateConverter.convert(transactionHistoryState) } override suspend fun getTxHistoryItems( @@ -366,7 +371,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain: Blockchain, derivationPath: String?, ): WalletManager? { - getWmInitializationMutex(blockchain, derivationPath).withLock { + getWmInitializationMutex(userWalletId, blockchain, derivationPath).withLock { val userWallet = getUserWallet(userWalletId) var walletManager = walletManagersStore.getSyncOrNull( @@ -738,15 +743,24 @@ internal class DefaultWalletManagersFacade @Inject constructor( return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED } - private fun getWmInitializationMutex(blockchain: Blockchain, derivationPath: String?): Mutex { - val key = createMutexMapKey(blockchain, derivationPath) + private fun getWmInitializationMutex( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): Mutex { + val key = createMutexMapKey(userWalletId, blockchain, derivationPath) return wmInitializationMutexes.computeIfAbsent(key) { Mutex() } } - private fun createMutexMapKey(blockchain: Blockchain, derivationPath: String?): String { - return blockchain.toNetworkId() + "|" + derivationPath + private fun createMutexMapKey(userWalletId: UserWalletId, blockchain: Blockchain, derivationPath: String?): String { + return listOf( + userWalletId.stringValue, + blockchain.toNetworkId(), + derivationPath, + ) + .joinToString(separator = "|") } private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { diff --git a/data/wallets/detekt-baseline-debug.xml b/data/wallets/detekt-baseline-debug.xml index 7ac40bc693..d8e411e670 100644 --- a/data/wallets/detekt-baseline-debug.xml +++ b/data/wallets/detekt-baseline-debug.xml @@ -11,7 +11,6 @@ MultilineLambdaItParameter:TangemHotWalletSigner.kt$TangemHotWalletSigner${ Timber.e(it) return if (it is TangemSdkError) { CompletionResult.Failure(it) } else { CompletionResult.Failure(TangemSdkError.ExceptionError(it)) } } NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, count, deadline, boot) NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, it.attempts, it.deadline, it.bootCount) - SuspendFunSwallowedCancellation:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor$runCatching SuspendFunSwallowedCancellation:TangemHotWalletSigner.kt$TangemHotWalletSigner$runCatching UnnecessaryLet:MissedDerivationsFinder.kt$MissedDerivationsFinder$let(::findByNetworks) UseOrEmpty:DefaultColdMapDerivationsRepository.kt$DefaultColdMapDerivationsRepository$oldKeys[walletKey] ?: emptyMap() diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 85ca6a00e5..c9844f8fd5 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-02 13:19:49 +Date: 2025-12-02 18:48:27 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1435 + Total Issues: 1424 Modules with Issues: 62 - Average Issues per Module: 23 + Average Issues per Module: 22 Progress: - Fixed: 367 out of 1802 (20%) - Remaining: 1435 + Fixed: 378 out of 1802 (20%) + Remaining: 1424 ========================================== All Modules with Issues (sorted by count) @@ -31,8 +31,8 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 148 features/markets/impl 148 +features/wallet/impl 146 features/onboarding-v2/impl 130 features/swap/impl 67 features/send-v2/impl 57 @@ -45,9 +45,9 @@ features/manage-tokens/impl 45 features/swap-v2/impl 40 domain/wallets 37 features/nft/impl 34 -features/tester/impl 31 -domain/tokens 28 +features/tester/impl 30 core/ui 27 +domain/tokens 26 common/ui 26 features/swap/domain 25 data/visa 22 @@ -55,16 +55,16 @@ features/yield-supply/impl 21 features/tangempay/details/impl 21 data/nft 20 features/swap/data 15 -data/wallets 14 +data/wallets 13 data/swap 13 features/token-recieve/impl 11 features/qr-scanning/impl 11 -domain/account/status 11 data/onramp 11 core/datasource 11 data/markets 10 features/details/impl 9 domain/staking 9 +domain/account/status 9 data/yield-supply 9 data/networks 9 features/referral/impl 8 @@ -72,7 +72,6 @@ domain/transaction 8 libs/tangem-sdk-api 7 data/txhistory 7 features/welcome/impl 6 -data/wallet-manager 6 libs/visa 5 features/send-v2/api 5 features/home/impl 5 @@ -87,6 +86,7 @@ features/txhistory/impl 3 features/tangempay/onboarding/impl 3 features/create-wallet-start/impl 3 domain/manage-tokens 3 +data/wallet-manager 3 data/manage-tokens 3 common/routing 3 features/account/api 2 diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 7e7cfa8521..e896b83ddf 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(projects.domain.referral) api(projects.domain.staking) api(projects.domain.tokens) + api(projects.domain.walletManager) api(projects.domain.wallets) implementation(projects.libs.blockchainSdk) diff --git a/domain/account/status/detekt-baseline-debug.xml b/domain/account/status/detekt-baseline-debug.xml index 1f66781ca0..c461b54d39 100644 --- a/domain/account/status/detekt-baseline-debug.xml +++ b/domain/account/status/detekt-baseline-debug.xml @@ -7,8 +7,6 @@ MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val currency = it.currency val isContractAddressMatch = contractAddress == null || currency.id.contractAddress.equals(contractAddress, ignoreCase = true) currency.network.rawId == networkId.rawId.value && currency.network.derivationPath.value == derivationPath.value && isContractAddressMatch } MultilineLambdaItParameter:ApplyTokenListSortingUseCaseV2.kt$ApplyTokenListSortingUseCaseV2${ errors[account.accountId] = it return@map account } MultilineLambdaItParameter:DefaultMultiAccountStatusListProducer.kt$DefaultMultiAccountStatusListProducer${ singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(it.walletId), ) } - MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ ExpressAsset.ID( networkId = it.network.backendId, contractAddress = (it as? CryptoCurrency.Token)?.contractAddress, ) } - MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ it.network.backendId == networkId && !it.isCustom && it.contractAddress.equals(contractAddress, true) } UnnecessaryAbstractClass:MultiAccountStatusListSupplier.kt$MultiAccountStatusListSupplier$MultiAccountStatusListSupplier UnnecessaryAbstractClass:SingleAccountStatusListSupplier.kt$SingleAccountStatusListSupplier$SingleAccountStatusListSupplier UnnecessaryAbstractClass:SingleAccountStatusSupplier.kt$SingleAccountStatusSupplier$SingleAccountStatusSupplier diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 70d95e70cd..32444c8e37 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -16,6 +16,7 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -95,6 +96,7 @@ internal object AccountStatusUseCaseModule { accountsCRUDRepository: AccountsCRUDRepository, currenciesRepository: CurrenciesRepository, derivationsRepository: DerivationsRepository, + walletManagersFacade: WalletManagersFacade, cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, stakingIdFactory: StakingIdFactory, networksCleaner: NetworksCleaner, @@ -107,6 +109,7 @@ internal object AccountStatusUseCaseModule { accountsCRUDRepository = accountsCRUDRepository, currenciesRepository = currenciesRepository, derivationsRepository = derivationsRepository, + walletManagersFacade = walletManagersFacade, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, stakingIdFactory = stakingIdFactory, networksCleaner = networksCleaner, @@ -120,7 +123,6 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton fun provideCryptoCurrencyBalanceFetcher( - accountsCRUDRepository: AccountsCRUDRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, @@ -128,7 +130,6 @@ internal object AccountStatusUseCaseModule { dispatchers: CoroutineDispatcherProvider, ): CryptoCurrencyBalanceFetcher { return CryptoCurrencyBalanceFetcher( - accountsCRUDRepository = accountsCRUDRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 9e4e862dda..51ffcfa1e8 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -21,8 +21,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.* import timber.log.Timber @@ -49,6 +51,7 @@ class ManageCryptoCurrenciesUseCase( private val accountsCRUDRepository: AccountsCRUDRepository, private val currenciesRepository: CurrenciesRepository, private val derivationsRepository: DerivationsRepository, + private val walletManagersFacade: WalletManagersFacade, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val networksCleaner: NetworksCleaner, @@ -83,6 +86,11 @@ class ManageCryptoCurrenciesUseCase( val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = add, remove = remove) + if (!modifiedCurrencyList.hasChanges) { + Timber.d("No changes in currencies, skipping") + return@withContext + } + saveAccount( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), ) @@ -90,17 +98,7 @@ class ManageCryptoCurrenciesUseCase( derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { - /* - * If only removal of currencies happened, we need to sync tokens. Otherwise, tokens will be synced - * when balances are refreshed for added currencies. - */ - val isOnlyRemoval = modifiedCurrencyList.added.isEmpty() && modifiedCurrencyList.removed.isNotEmpty() - - if (isOnlyRemoval) { - launch { accountsCRUDRepository.syncTokens(userWalletId) } - clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) - return@launch - } + syncTokens(userWalletId, modifiedCurrencyList) cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) @@ -121,10 +119,10 @@ class ManageCryptoCurrenciesUseCase( val foundToken = accountStatus.tokenList.flattenCurrencies() .mapNotNull { it.currency as? CryptoCurrency.Token } - .firstOrNull { - it.network.backendId == networkId && - !it.isCustom && - it.contractAddress.equals(contractAddress, true) + .firstOrNull { token -> + token.network.backendId == networkId && + !token.isCustom && + token.contractAddress.equals(contractAddress, true) } if (foundToken != null) return@withContext foundToken @@ -137,6 +135,8 @@ class ManageCryptoCurrenciesUseCase( saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) parallelUpdatingScope.launch { + syncTokens(userWalletId, modifiedCurrencyList) + cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) } @@ -260,15 +260,40 @@ class ManageCryptoCurrenciesUseCase( ) } + private suspend fun syncTokens(userWalletId: UserWalletId, modifiedCurrencyList: ModifiedCurrencyList) { + createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + + runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } + .onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") } + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + runSuspendCatching { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + .onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") } + } + } + private suspend fun refreshExpress(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) return coroutineScope { launch { - val assetIds = currencies.mapTo(hashSetOf()) { + val assetIds = currencies.mapTo(hashSetOf()) { currency -> ExpressAsset.ID( - networkId = it.network.backendId, - contractAddress = (it as? CryptoCurrency.Token)?.contractAddress, + networkId = currency.network.backendId, + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) } @@ -325,5 +350,8 @@ class ManageCryptoCurrenciesUseCase( val added: List, val removed: List, val total: List, - ) + ) { + + val hasChanges get() = added.isNotEmpty() || removed.isNotEmpty() + } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt index f492131ba6..ebd48e4b0b 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt @@ -1,8 +1,6 @@ package com.tangem.domain.account.status.utils import arrow.core.Either -import arrow.core.raise.either -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher @@ -19,7 +17,6 @@ import timber.log.Timber * Utility class responsible for fetching and refreshing the balances of various crypto currencies * associated with a user's wallet. * - * @property accountsCRUDRepository Repository for managing account data. * @property multiNetworkStatusFetcher Fetcher for updating network statuses. * @property multiQuoteStatusFetcher Fetcher for updating quote statuses. * @property multiYieldBalanceFetcher Fetcher for updating yield balances. @@ -29,7 +26,6 @@ import timber.log.Timber [REDACTED_AUTHOR] */ class CryptoCurrencyBalanceFetcher( - private val accountsCRUDRepository: AccountsCRUDRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, @@ -80,22 +76,13 @@ class CryptoCurrencyBalanceFetcher( private suspend fun refreshNetworks( userWalletId: UserWalletId, currencies: List, - ): Either = either { - val either = multiNetworkStatusFetcher( + ): Either { + return multiNetworkStatusFetcher( params = MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network), ), ) - - arrow.core.raise.catch( - block = { accountsCRUDRepository.syncTokens(userWalletId) }, - catch = { - Timber.e(it, "Failed to sync tokens for wallet: $userWalletId") - }, - ) - - return either } private suspend fun refreshYieldBalances( diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index e5fffdace4..484176877c 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.legacy) + implementation(tangemDeps.blockchain) + /* Core */ api(projects.core.pagination) testImplementation(projects.core.pagination) diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 51fbc16bab..aa30b2a392 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -61,6 +61,8 @@ class SaveManagedTokensUseCase( parallelUpdatingScope.launch { withContext(NonCancellable) { + syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies) + launch { refreshUpdatedNetworks( userWalletId = userWalletId, @@ -96,6 +98,26 @@ class SaveManagedTokensUseCase( ) } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( @@ -103,8 +125,6 @@ class SaveManagedTokensUseCase( networks = addedCurrencies.map(CryptoCurrency::network).toSet(), ), ) - - currenciesRepository.syncTokens(userWalletId) } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 735657a25b..c783650c9d 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { api(projects.domain.networks) api(projects.domain.staking) api(projects.domain.quotes) + api(projects.domain.walletManager) api(projects.domain.wallets) api(projects.domain.wallets.models) api(projects.domain.promo) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index c1d1b50552..b6e00f2b78 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable @@ -31,6 +32,7 @@ import kotlinx.coroutines.withContext class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, + private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, @@ -80,6 +82,8 @@ class SaveMarketTokensUseCase( parallelUpdatingScope.launch { withContext(NonCancellable) { + syncTokens(userWalletId, savedCurrencies) + launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) } launch { refreshUpdatedYieldBalances(userWalletId, savedCurrencies) } launch { refreshUpdatedQuotes(savedCurrencies) } @@ -88,6 +92,26 @@ class SaveMarketTokensUseCase( } } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( @@ -95,7 +119,6 @@ class SaveMarketTokensUseCase( networks = addedCurrencies.map(CryptoCurrency::network).toSet(), ), ) - currenciesRepository.syncTokens(userWalletId) } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/tokens/detekt-baseline-debug.xml b/domain/tokens/detekt-baseline-debug.xml index f1b234f537..695177702e 100644 --- a/domain/tokens/detekt-baseline-debug.xml +++ b/domain/tokens/detekt-baseline-debug.xml @@ -6,8 +6,6 @@ CanBeNonNullable:BaseActionsFactory.kt$BaseActionsFactory$requirementsDeferred: Deferred<AssetRequirementsCondition?>? CanBeNonNullable:CommonActionsFactory.kt$CommonActionsFactory$swapUnavailableReasonDeferred: Deferred<ScenarioUnavailabilityReason>? ExplicitCollectionElementAccessMethod:GetWalletTotalBalanceUseCase.kt$GetWalletTotalBalanceUseCase$walletBalanceCache.put(userWalletId, content) - MultilineLambdaItParameter:AddCryptoCurrenciesUseCase.kt$AddCryptoCurrenciesUseCase${ it.network.backendId == networkId && !it.isCustom && it.contractAddress.equals(contractAddress, true) } - MultilineLambdaItParameter:AddCryptoCurrenciesUseCase.kt$AddCryptoCurrenciesUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } MultilineLambdaItParameter:BaseCurrencyStatusOperations.kt$BaseCurrencyStatusOperations${ singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = it)) .firstOrNull() } MultilineLambdaItParameter:BaseCurrencyStatusOperations.kt$BaseCurrencyStatusOperations${ val exception = IllegalStateException("$it") Error.DataError(exception) } MultilineLambdaItParameter:FetchCurrencyStatusUseCase.kt$FetchCurrencyStatusUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index f6896dbad8..19357b3e82 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -14,6 +14,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -28,6 +29,7 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, + private val walletManagersFacade: WalletManagersFacade, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -73,9 +75,11 @@ class AddCryptoCurrenciesUseCase( ) val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - addCurrencies(userWalletId, currencyToAdd) + val addedCurrencies = addCurrencies(userWalletId, currencyToAdd) coroutineScope { + syncTokens(userWalletId, addedCurrencies) + awaitAll( async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, async { refreshUpdatedYieldBalances(userWalletId, currencyToAdd) }, @@ -102,18 +106,20 @@ class AddCryptoCurrenciesUseCase( val foundToken = existingCurrencies .filterIsInstance() - .firstOrNull { - it.network.backendId == networkId && - !it.isCustom && - it.contractAddress.equals(contractAddress, true) + .firstOrNull { token -> + token.network.backendId == networkId && + !token.isCustom && + token.contractAddress.equals(contractAddress, true) } if (foundToken != null) { return@either foundToken } val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - addCurrencies(userWalletId, tokenToAdd) + val addedCurrencies = addCurrencies(userWalletId, tokenToAdd) coroutineScope { + syncTokens(userWalletId = userWalletId, addedCurrencies = addedCurrencies) + awaitAll( async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, async { refreshUpdatedYieldBalances(userWalletId, tokenToAdd) }, @@ -124,6 +130,26 @@ class AddCryptoCurrenciesUseCase( tokenToAdd } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + /** * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. @@ -149,8 +175,6 @@ class AddCryptoCurrenciesUseCase( networks = setOfNotNull(networksToUpdate, networkToUpdate), ), ) - - currenciesRepository.syncTokens(userWalletId) } private suspend fun refreshUpdatedYieldBalances( @@ -162,9 +186,9 @@ class AddCryptoCurrenciesUseCase( currencyId = addedCurrency.id, network = addedCurrency.network, ) - .getOrElse { - when (it) { - is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + .getOrElse { error -> + when (error) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$error")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } @@ -205,12 +229,14 @@ class AddCryptoCurrenciesUseCase( ) } - private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, currency: CryptoCurrency) { - catch( - { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, - ) { - raise(it) - } + private suspend fun Raise.addCurrencies( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): List { + return catch( + block = { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, + catch = ::raise, + ) } /** diff --git a/features/tester/impl/detekt-baseline-debug.xml b/features/tester/impl/detekt-baseline-debug.xml index 4a5ba6a9ce..d115b8f9b6 100644 --- a/features/tester/impl/detekt-baseline-debug.xml +++ b/features/tester/impl/detekt-baseline-debug.xml @@ -9,7 +9,6 @@ ExplicitCollectionElementAccessMethod:TestPushAddKeyDataTransformer.kt$TestPushAddKeyDataTransformer$mutableData.set(index = index, updated) ExplicitCollectionElementAccessMethod:TestPushAddValueDataTransformer.kt$TestPushAddValueDataTransformer$mutableData.set(index = index, updated) MaxChainedCallsOnSameLine:BlockchainProvidersScreen.kt$ProvidersDnDTarget$event.toAndroidDragEvent().clipData.getItemAt(0).text.toString().toInt() - MultilineLambdaItParameter:ApiEnvironmentComparator.kt$ApiEnvironmentComparator${ when (it) { ApiEnvironment.DEV -> 0 ApiEnvironment.DEV_2 -> 1 ApiEnvironment.DEV_3 -> 2 ApiEnvironment.STAGE -> 3 ApiEnvironment.MOCK -> 4 ApiEnvironment.PROD -> 5 } } MultilineLambdaItParameter:BlockchainProvidersScreen.kt${ value = it state.onValueChange(it.text) } MultilineLambdaItParameter:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ if (it.blockchainId == blockchainId) { it.update() } else { it } } MultilineLambdaItParameter:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ it.copyProvidersUM(blockchainId = id) { copy( addPublicProviderDialog = addPublicProviderDialog.copy( hasError = !PatternsCompat.WEB_URL.matcher(url).matches(), ), ) } } diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index c3993c7c7e..c83e37ad72 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -85,7 +85,6 @@ MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } - MultilineLambdaItParameter:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { it.walletCardState.id } else { null } } NamedArguments:BasicAccountListSubscriber.kt$BasicAccountListSubscriber$updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) @@ -109,7 +108,6 @@ NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NoNameShadowing:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ it is WalletNotification.FinishWalletActivation } NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} From 21d8c590c180d028150f83bad22f9838e42a743a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Dec 2025 18:32:18 +0500 Subject: [PATCH 22/30] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 26 ++++++++++++++++--- .../intents/WalletWarningsClickIntents.kt | 1 + .../common/preview/WalletScreenPreviewData.kt | 1 + .../analytics/WalletScreenAnalyticsEvent.kt | 25 +++++++++++++++++- .../utils/WalletWarningsAnalyticsSender.kt | 9 ++++++- .../domain/GetMultiWalletWarningsFactory.kt | 1 + .../wallet/state/model/WalletNotification.kt | 1 + 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 97c11fb923..ec0be702e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -4,9 +4,11 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss +import com.squareup.sqldelight.internal.AtomicBoolean import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -100,6 +102,7 @@ internal class WalletModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val tangemPayMainInfoManager: TangemPayMainInfoManager, + private val trackingContextProxy: TrackingContextProxy, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -115,9 +118,12 @@ internal class WalletModel @Inject constructor( private val updateTangemPayJobHolder = JobHolder() private var expressTxStatusTaskScheduler = SingleTaskScheduler() + private val hasMainScreenOpenedEventSent = AtomicBoolean(false) init { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + if (!hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy) + } suggestToOpenMarkets() @@ -274,11 +280,25 @@ internal class WalletModel @Inject constructor( // It's okay here because we need to be able to observe the selected wallet changes @Suppress("DEPRECATION") private fun subscribeOnSelectedWalletFlow() { - getSelectedWalletUseCase().onRight { - it + getSelectedWalletUseCase().onRight { walletFlow -> + walletFlow .conflate() .distinctUntilChanged() .onEach { selectedWallet -> + trackingContextProxy.setContext(selectedWallet) + + if (hotWalletFeatureToggles.isHotWalletEnabled && !hasMainScreenOpenedEventSent.get()) { + // send it here because we need context to be set + modelScope.launch { + val hasMobileWallet = userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot } + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(hasMobileWallet), + ) + hasMainScreenOpenedEventSent.set(true) + } + } + if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 1fdec44c36..197cb45b57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -458,6 +458,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onFinishWalletActivationClick(isBackupExists: Boolean) { + analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation) val userWalletId = stateHolder.getSelectedWalletId() appRouter.push(WalletActivation(userWalletId, isBackupExists)) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 4e0c498af1..a14bd829de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -172,6 +172,7 @@ internal object WalletScreenPreviewData { text = resourceReference(R.string.hw_activation_need_finish), onClick = { }, ), + isBackupExists = false, ), ), bottomSheetConfig = null, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 54d3a4cca2..9f5263273e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -51,7 +51,30 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - data object ScreenOpened : MainScreen(event = "Screen opened") + data object ScreenOpenedLegacy : MainScreen( + event = "Screen opened", + ) + + data class ScreenOpened( + private val hasMobileWallet: Boolean, + ) : MainScreen( + event = "Screen opened", + params = mapOf("Mobile Wallet" to if (hasMobileWallet) "Yes" else "No"), + ) + + data class NoticeFinishActivation(private val activationState: ActivationState) : MainScreen( + event = "Notice - Finish Activation", + params = mapOf("Activation State" to activationState.value), + ) { + enum class ActivationState(val value: String) { + NotStarted("Not Started"), + Unfinished("Unfinished"), + } + } + + data object ButtonFinalizeActivation : MainScreen( + event = "Button - Finalize Activation", + ) class WalletSelected(val isImported: Boolean) : MainScreen( event = "Wallet Selected", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index b080fd487e..de9c90b20e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -70,9 +70,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.NetworksUnreachable, is WalletNotification.UsedOutdatedData, is WalletNotification.UnlockVisaAccess, - is WalletNotification.FinishWalletActivation, is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event -> null + is WalletNotification.FinishWalletActivation -> { + val activationState = if (warning.isBackupExists) { + MainScreen.NoticeFinishActivation.ActivationState.Unfinished + } else { + MainScreen.NoticeFinishActivation.ActivationState.NotStarted + } + MainScreen.NoticeFinishActivation(activationState) + } is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 457df41e5e..79df423642 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -477,6 +477,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, ) }, + isBackupExists = isBackupExists, ), condition = shouldShowFinishActivation, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 0ddf1d2139..9793c879cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -290,6 +290,7 @@ sealed class WalletNotification(val config: NotificationConfig) { data class FinishWalletActivation( val type: WalletActivationBannerType, val buttonsState: ButtonsState, + val isBackupExists: Boolean, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), From ddda403196559e4ded3ff30eb4f1dfa638a5f720 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Dec 2025 18:27:47 +0300 Subject: [PATCH 23/30] Updated on 2026-08-14 --- .../com/tangem/tests/send/addressScreen/RecentBlockTest.kt | 5 ++++- .../com/tangem/tests/send/warnings/SolanaWarningsTest.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt index 1a169ead0b..d7f75da532 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt index ec60a4de62..976cfeb85f 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt @@ -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) From a291a9a8c0125416f131edaf845a86a09b2a1c55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Dec 2025 11:33:25 +0200 Subject: [PATCH 24/30] Updated on 2026-08-14 --- .../MockP2PEthPoolAccountResponseFactory.kt | 82 +++++++ .../datasource/di/StakingStoreModule.kt | 19 ++ data/staking/build.gradle.kts | 1 + .../ethpool/P2PYieldBalanceConverter.kt | 92 ++++++++ .../staking/di/YieldBalanceSupplierModule.kt | 20 ++ .../multi/DefaultMultiYieldBalanceFetcher.kt | 173 ++++++++++++-- .../multi/DefaultMultiYieldBalanceProducer.kt | 15 +- .../staking/store/DefaultP2PBalancesStore.kt | 213 ++++++++++++++++++ .../data/staking/store/P2PBalancesStore.kt | 29 +++ .../tangem/data/staking/StakingBalanceExt.kt | 28 +++ .../tangem/data/staking/YieldBalanceExt.kt | 10 - .../DefaultMultiYieldBalanceFetcherTest.kt | 9 + .../DefaultMultiYieldBalanceProducerTest.kt | 95 +++++++- .../tangem/domain/models/staking/StakingID.kt | 10 +- .../domain/staking/model/StakingOption.kt | 2 +- 15 files changed, 767 insertions(+), 31 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt delete mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt new file mode 100644 index 0000000000..1e814c5d9f --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 695c1c8d45..aeb4d1b8d9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -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>> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "p2p_balances") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + @Provides @Singleton fun provideP2PEthPoolVaultsStore( diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index a01398d354..fc7059b626 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt new file mode 100644 index 0000000000..373a5de22f --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.data.staking.converters.ethpool + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* +import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import java.math.BigDecimal + +/** + * tmp solution before facade implementation + */ +internal object P2PYieldBalanceConverter { + + private const val ETH_DECIMALS = 18 + private const val ETH_SYMBOL = "ETH" + private const val ETH_NAME = "Ethereum" + private const val ETH_COINGECKO_ID = "ethereum" + + fun convert( + account: P2PEthPoolAccount, + vault: P2PEthPoolVault, + address: String, + source: StatusSource, + ): YieldBalance { + val integrationId = "p2p-ethereum-pooled" + val stakingId = StakingID( + integrationId = integrationId, + address = address, + ) + + val balanceItems = buildBalanceItems(account, vault) + + return if (balanceItems.isEmpty()) { + YieldBalance.Empty(stakingId = stakingId, source = source) + } else { + YieldBalance.Data( + stakingId = stakingId, + source = source, + balance = YieldBalanceItem( + items = balanceItems, + integrationId = integrationId, + ), + ) + } + } + + private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List = buildList { + if (account.stake.assets > BigDecimal.ZERO) { + add( + createBalanceItem( + groupId = "p2p-staked", + amount = account.stake.assets, + type = BalanceType.STAKED, + validatorAddress = vault.vaultAddress, + ), + ) + } + } + + private fun createBalanceItem( + groupId: String, + amount: BigDecimal, + type: BalanceType, + validatorAddress: String, + ): BalanceItem { + return BalanceItem( + groupId = groupId, + token = createEthToken(), + type = type, + amount = amount, + rawCurrencyId = ETH_COINGECKO_ID, + validatorAddress = validatorAddress, + date = null, + pendingActions = emptyList(), + pendingActionsConstraints = emptyList(), + isPending = false, + ) + } + + private fun createEthToken(): YieldToken { + return YieldToken( + name = ETH_NAME, + network = NetworkType.ETHEREUM, + symbol = ETH_SYMBOL, + decimals = ETH_DECIMALS, + address = null, + coinGeckoId = ETH_COINGECKO_ID, + logoURI = null, + isPoints = false, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt index 3ef90f5b74..69c635bb0c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt @@ -1,15 +1,20 @@ package com.tangem.data.staking.di import androidx.datastore.core.DataStore +import com.tangem.data.staking.store.DefaultP2PBalancesStore import com.tangem.data.staking.store.DefaultYieldsBalancesStore +import com.tangem.data.staking.store.P2PBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -33,6 +38,21 @@ internal object YieldBalanceSupplierModule { ) } + @Provides + @Singleton + fun provideP2PBalancesStore( + persistenceStore: DataStore>>, + p2pVaultsStore: P2PEthPoolVaultsStore, + dispatchers: CoroutineDispatcherProvider, + ): P2PBalancesStore { + return DefaultP2PBalancesStore( + runtimeStore = RuntimeSharedStore(), + persistenceStore = persistenceStore, + vaultsProvider = { runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty() }, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun provideSingleYieldBalanceSupplier(factory: SingleYieldBalanceProducer.Factory): SingleYieldBalanceSupplier { diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index eca2f69f33..a7bcd2cdbb 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -5,12 +5,17 @@ import arrow.core.left import arrow.core.right import arrow.core.toOption import com.tangem.data.common.api.safeApiCall +import com.tangem.data.staking.store.P2PBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.ethpool.P2PEthPoolApi +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody 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.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.utils.catchOn @@ -18,30 +23,43 @@ import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject /** * Default implementation of [MultiYieldBalanceFetcher] * - * @property userWalletsStore user wallets store - * @property stakingYieldsStore staking yields store - * @property yieldsBalancesStore yields balances store - * @property stakeKitApi stake kit API - * @property dispatchers dispatchers + * Supports both StakeKit and P2P staking providers. + * + * @property userWalletsStore user wallets store + * @property stakingYieldsStore staking yields store + * @property yieldsBalancesStore yields balances store (StakeKit) + * @property p2pBalancesStore P2P balances store + * @property stakeKitApi stake kit API + * @property p2pApi P2P ETH Pool API + * @property p2pVaultsStore P2P vaults store + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultMultiYieldBalanceFetcher @Inject constructor( private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, + private val p2pBalancesStore: P2PBalancesStore, private val stakeKitApi: StakeKitApi, + private val p2pApi: P2PEthPoolApi, + private val p2pVaultsStore: P2PEthPoolVaultsStore, private val dispatchers: CoroutineDispatcherProvider, ) : MultiYieldBalanceFetcher { @@ -57,25 +75,150 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( return it.left() } - Timber.i("Staking IDs to fetch:\n${stakingIds.joinToString("\n")}") + val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId -> + val stakingIntegrationID = StakingIntegrationID.entries.find { + it.value == stakingId.integrationId + } + stakingIntegrationID is StakingIntegrationID.StakeKit + } + + Timber.i( + """ + Staking IDs to fetch: + - StakeKit: ${stakeKitIds.joinToString()} + - P2P: ${p2pIds.joinToString()} + """.trimIndent(), + ) return Either.catchOn(dispatchers.default) { - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = stakingIds) + coroutineScope { + if (stakeKitIds.isNotEmpty()) { + launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) } + } - val availableStakingIds = getAvailableStakingIds( - userWalletId = params.userWalletId, - stakingIds = stakingIds, - ) - - fetch(userWalletId = params.userWalletId, stakingIds = availableStakingIds) + if (p2pIds.isNotEmpty()) { + launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) } + } + } } .onLeft { throwable -> Timber.e(throwable, "Unable to fetch yield balances $params") - yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) + if (stakeKitIds.isNotEmpty()) { + yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakeKitIds.toSet()) + } + if (p2pIds.isNotEmpty()) { + p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet()) + } } } + private suspend fun fetchStakeKitBalances(userWalletId: UserWalletId, stakingIds: Set) { + yieldsBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val availableStakingIds = getAvailableStakingIds( + userWalletId = userWalletId, + stakingIds = stakingIds, + ) + + fetchFromStakeKit(userWalletId = userWalletId, stakingIds = availableStakingIds) + } + + private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set) { + p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty() + if (vaults.isEmpty()) { + Timber.w("No P2P vaults available for $userWalletId") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + return + } + + fetchFromP2P(userWalletId = userWalletId, stakingIds = stakingIds, vaults = vaults) + } + + private suspend fun fetchFromP2P( + userWalletId: UserWalletId, + stakingIds: Set, + vaults: List, + ) { + safeApiCall( + call = { + val addresses = stakingIds.map { it.address }.toSet() + + val responses = mutableSetOf() + + for (vault in vaults) { + for (address in addresses) { + runSuspendCatching { + val response = p2pApi.getAccountInfo( + network = P2PEthPoolNetwork.MAINNET.value, + delegatorAddress = address, + vaultAddress = vault.vaultAddress, + ) + + when (response) { + is ApiResponse.Success -> { + val data = response.data + if (data.error != null) { + Timber.w( + "P2P API returned error for vault ${vault.vaultAddress}, " + + "address $address: ${data.error ?: "error"}", + ) + } else { + val result = requireNotNull(data.result) { + "Result is null in successful response" + } + responses.add(result) + } + } + is ApiResponse.Error -> { + Timber.w( + response.cause, + "Failed to fetch P2P balance for vault ${vault.vaultAddress}, " + + "address $address", + ) + } + } + }.onFailure { error -> + Timber.w( + error, + "Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address", + ) + } + } + } + + Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId") + + if (responses.isNotEmpty()) { + p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses) + + val missingStakingIds = stakingIds.filter { stakingId -> + responses.none { response -> + response.delegatorAddress.equals(stakingId.address, ignoreCase = true) + } + } + + if (missingStakingIds.isNotEmpty()) { + Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet()) + } + } else { + Timber.i("No P2P responses received for $userWalletId") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + } + }, + onError = { throwable -> + Timber.e(throwable, "Unable to fetch P2P balances $userWalletId") + + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + + throw throwable + }, + ) + } + private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() @@ -139,7 +282,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( return yieldsIds } - private suspend fun fetch(userWalletId: UserWalletId, stakingIds: Set) { + private suspend fun fetchFromStakeKit(userWalletId: UserWalletId, stakingIds: Set) { safeApiCall( call = { val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create) diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt index f66ec71196..9d4f4917b0 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt @@ -2,6 +2,7 @@ package com.tangem.data.staking.multi import arrow.core.Option import arrow.core.some +import com.tangem.data.staking.store.P2PBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer @@ -10,6 +11,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEmpty @@ -17,8 +19,11 @@ import kotlinx.coroutines.flow.onEmpty /** * Default implementation of [MultiYieldBalanceProducer] * + * Combines yield balances from both StakeKit and P2P providers. + * * @property params params - * @property yieldsBalancesStore yields balances store + * @property yieldsBalancesStore StakeKit yields balances store + * @property p2pBalancesStore P2P balances store * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -26,13 +31,19 @@ import kotlinx.coroutines.flow.onEmpty internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( @Assisted val params: MultiYieldBalanceProducer.Params, private val yieldsBalancesStore: YieldsBalancesStore, + private val p2pBalancesStore: P2PBalancesStore, private val dispatchers: CoroutineDispatcherProvider, ) : MultiYieldBalanceProducer { override val fallback: Option> = emptySet().some() override fun produce(): Flow> { - return yieldsBalancesStore.get(userWalletId = params.userWalletId) + val stakeKitFlow = yieldsBalancesStore.get(userWalletId = params.userWalletId) + val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId) + + return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances -> + stakeKitBalances + p2pBalances + } .distinctUntilChanged() .onEmpty { emit(value = hashSetOf()) } .flowOn(dispatchers.default) diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt new file mode 100644 index 0000000000..24ff66cec1 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt @@ -0,0 +1,213 @@ +package com.tangem.data.staking.store + +import androidx.datastore.core.DataStore +import com.tangem.data.staking.converters.ethpool.P2PEthPoolAccountConverter +import com.tangem.data.staking.converters.ethpool.P2PYieldBalanceConverter +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import timber.log.Timber + +internal typealias WalletIdWithP2PBalances = Map> +internal typealias WalletIdWithP2PResponses = Map> + +/** + * Default implementation of [P2PBalancesStore] + * + * Stores P2P ETH Pool staking balances with persistence support. + * + * @property runtimeStore runtime store + * @property persistenceStore persistence store + * @property vaultsProvider provider for vaults + * @param dispatchers coroutine dispatchers + */ +internal class DefaultP2PBalancesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceStore: DataStore, + private val vaultsProvider: suspend () -> List, + dispatchers: CoroutineDispatcherProvider, +) : P2PBalancesStore { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedData = persistenceStore.data.firstOrNull() ?: return@launch + val vaults = vaultsProvider() + + runtimeStore.store( + value = cachedData.map { (stringWalletId, responses) -> + val key = UserWalletId(stringWalletId) + val value = responses.mapNotNull { response -> + val vault = vaults.firstOrNull { it.vaultAddress == response.vaultAddress } + ?: return@mapNotNull null + val account = P2PEthPoolAccountConverter.convert(response) + P2PYieldBalanceConverter.convert( + account = account, + vault = vault, + address = account.delegatorAddress, + source = StatusSource.CACHE, + ) + }.toSet() + + key to value + }.toMap(), + ) + } + } + + override fun get(userWalletId: UserWalletId): Flow> { + return runtimeStore.get().map { it[userWalletId].orEmpty() } + } + + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { + return runtimeStore.getSyncOrNull() + ?.get(userWalletId) + ?.firstOrNull { it.stakingId == stakingId } + } + + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + return runtimeStore.getSyncOrNull()?.get(userWalletId) + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) { + refresh(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) { + updateInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { + it.copySealed(source = StatusSource.CACHE) + } + } + + override suspend fun storeActual(userWalletId: UserWalletId, values: Set) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, values = values) } + launch { storeInPersistence(userWalletId = userWalletId, values = values) } + } + } + + override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) { + updateInRuntime( + userWalletId = userWalletId, + stakingIds = stakingIds, + ifNotFound = ::createErrorYieldBalance, + update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, + ) + } + + override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) { + coroutineScope { + launch { clearInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) } + launch { clearInPersistence(userWalletId = userWalletId, stakingIds = stakingIds) } + } + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { + val vaults = vaultsProvider() + + val newBalances = values.mapNotNull { response -> + val vault = vaults.firstOrNull { it.vaultAddress == response.vaultAddress } + if (vault == null) { + Timber.w("Vault not found for ${response.vaultAddress}") + return@mapNotNull null + } + + val account = P2PEthPoolAccountConverter.convert(response) + P2PYieldBalanceConverter.convert( + account = account, + vault = vault, + address = account.delegatorAddress, + source = StatusSource.ACTUAL, + ) + }.toSet() + + runtimeStore.update(default = emptyMap()) { saved -> + saved.toMutableMap().apply { + this[userWalletId] = saved[userWalletId] + ?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId } + ?: newBalances + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = this[userWalletId.stringValue] + ?.addOrReplace(values) { old, new -> + old.delegatorAddress == new.delegatorAddress && old.vaultAddress == new.vaultAddress + } + ?: values + } + } + } + + private suspend fun clearInRuntime(userWalletId: UserWalletId, stakingIds: Set) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + this[userWalletId] = this[userWalletId].orEmpty() + .filterNot { it.stakingId in stakingIds } + .toSet() + } + } + } + + private suspend fun clearInPersistence(userWalletId: UserWalletId, stakingIds: Set) { + val integrationIds = stakingIds.map { it.integrationId }.toSet() + + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty() + .filterNot { response -> + val responseIntegrationId = "p2p-ethereum-pooled:${response.vaultAddress}" + responseIntegrationId in integrationIds + } + .toSet() + } + } + } + + private suspend fun updateInRuntime( + userWalletId: UserWalletId, + stakingIds: Set, + ifNotFound: (StakingID) -> YieldBalance? = { null }, + update: (YieldBalance) -> YieldBalance, + ) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val portfolioBalances = stored[userWalletId].orEmpty() + + val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId -> + val balance = portfolioBalances + .firstOrNull { it.stakingId == stakingId } + ?: ifNotFound(stakingId) + ?: return@mapNotNullTo null + + update(balance) + } + + val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> + old.stakingId == new.stakingId + } + + put(key = userWalletId, value = updatedBalances) + } + } + } + + private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt new file mode 100644 index 0000000000..7dbf4b1abe --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt @@ -0,0 +1,29 @@ +package com.tangem.data.staking.store + +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Store for P2P ETH Pool staking balances + */ +interface P2PBalancesStore { + + fun get(userWalletId: UserWalletId): Flow> + + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? + + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + + suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) + + suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) + + suspend fun storeActual(userWalletId: UserWalletId, values: Set) + + suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) + + suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt new file mode 100644 index 0000000000..968fb21321 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking + +import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory +import com.tangem.data.staking.converters.ethpool.P2PEthPoolAccountConverter +import com.tangem.data.staking.converters.ethpool.P2PYieldBalanceConverter +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault + +internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { + return YieldBalanceConverter(source = source).convert(this)!! +} + +internal fun P2PEthPoolAccountResponse.toDomain( + vault: P2PEthPoolVault = MockP2PEthPoolAccountResponseFactory.createMockVault(vaultAddress = vaultAddress), + source: StatusSource = StatusSource.CACHE, +): YieldBalance { + val account = P2PEthPoolAccountConverter.convert(this) + return P2PYieldBalanceConverter.convert( + account = account, + vault = vault, + address = account.delegatorAddress, + source = source, + ) +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt deleted file mode 100644 index 09b56e3b9c..0000000000 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.token.converter.YieldBalanceConverter -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.staking.YieldBalance - -internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { - return YieldBalanceConverter(source = source).convert(this)!! -} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt index f7c56f3dc3..3e0c59cd8b 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -4,12 +4,15 @@ import arrow.core.toOption import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.data.staking.store.P2PBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.staking.StakingID @@ -33,13 +36,19 @@ internal class DefaultMultiYieldBalanceFetcherTest { private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true) + private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true) private val stakeKitApi: StakeKitApi = mockk() + private val p2pApi: P2PEthPoolApi = mockk() + private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk() private val fetcher = DefaultMultiYieldBalanceFetcher( userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, + p2pBalancesStore = p2pBalancesStore, stakeKitApi = stakeKitApi, + p2pApi = p2pApi, + p2pVaultsStore = p2pVaultsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt index 08fada9524..3435a0e034 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt @@ -2,10 +2,12 @@ package com.tangem.data.staking.multi import com.google.common.truth.Truth import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory +import com.tangem.data.staking.store.P2PBalancesStore import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toDomain -import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.test.core.getEmittedValues @@ -25,11 +27,13 @@ internal class DefaultMultiYieldBalanceProducerTest { private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011")) private val yieldsBalancesStore = mockk() + private val p2pBalancesStore = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultMultiYieldBalanceProducer( params = params, yieldsBalancesStore = yieldsBalancesStore, + p2pBalancesStore = p2pBalancesStore, dispatchers = dispatchers, ) @@ -43,11 +47,13 @@ internal class DefaultMultiYieldBalanceProducerTest { val networksStatusesFlow = flowOf(balances) every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) val actual = producer.produce() // check after producer.produce() verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } val values = getEmittedValues(flow = actual) @@ -60,11 +66,13 @@ internal class DefaultMultiYieldBalanceProducerTest { val networksStatusesFlow = MutableSharedFlow>(replay = 2) every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) val actual = producer.produce() // check after producer.produce() verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } // first emit val balances = setOf( @@ -99,11 +107,13 @@ internal class DefaultMultiYieldBalanceProducerTest { val networksStatusesFlow = MutableSharedFlow>(replay = 2) every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) val actual = producer.produce() // check after producer.produce() verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } // first emit val wrappers = setOf( @@ -146,11 +156,13 @@ internal class DefaultMultiYieldBalanceProducerTest { .buffer(capacity = 5) every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) val actual = producer.produceWithFallback() // check after producer.produce() verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } val values1 = getEmittedValues(flow = actual) @@ -168,11 +180,13 @@ internal class DefaultMultiYieldBalanceProducerTest { @Test fun `test that flow is empty`() = runTest { every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow() + every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow() val actual = producer.produce() // check after producer.produce() verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } val values = getEmittedValues(flow = actual) @@ -180,6 +194,58 @@ internal class DefaultMultiYieldBalanceProducerTest { Truth.assertThat(values).isEqualTo(listOf(emptySet())) } + @Test + fun `test that StakeKit and P2P balances are combined`() = runTest { + val stakeKitBalances = createStakeKitBalances() + val p2pBalances = createP2PBalances() + + every { yieldsBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances) + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances) + } + + @Test + fun `test that P2P balances are updated independently from StakeKit`() = runTest { + val stakeKitBalances = createStakeKitBalancesWithTonOnly() + val p2pFlow = MutableSharedFlow>(replay = 2) + + every { yieldsBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + // first emit - empty P2P + p2pFlow.emit(emptySet()) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances) + + // second emit - with P2P balance + val p2pBalances = createP2PBalances() + p2pFlow.emit(p2pBalances) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances) + } + private companion object { val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId @@ -187,5 +253,30 @@ internal class DefaultMultiYieldBalanceProducerTest { integrationId = "solana-sol-native-multivalidator-staking", address = "0x1", ) + val p2pEthereumId = StakingID( + integrationId = "p2p-ethereum-pooled", + address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84", + ) + + fun createStakeKitBalances(): Set { + return setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + } + + fun createStakeKitBalancesWithTonOnly(): Set { + return setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + ) + } + + fun createP2PBalances(): Set { + return setOf( + MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain( + source = StatusSource.ACTUAL, + ), + ) + } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt index 5ed3a0b3ce..e684f70531 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt @@ -2,5 +2,13 @@ package com.tangem.domain.models.staking import kotlinx.serialization.Serializable +enum class StakingProvider { + STAKEKIT, + P2P, +} + @Serializable -data class StakingID(val integrationId: String, val address: String) \ No newline at end of file +data class StakingID( + val integrationId: String, + val address: String, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt index 1a4563d0b5..d199edb836 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt @@ -41,7 +41,7 @@ sealed interface StakingOption { */ data class P2P(val vault: P2PEthPoolVault) : StakingOption { override val integrationId: String = - "p2p-ethereum-pooled:${vault.vaultAddress}" + "p2p-ethereum-pooled" override val apy: SerializedBigDecimal = vault.apy override val token: YieldToken = createEthToken() override val isAvailable: Boolean = !vault.isPrivate From 1bae7d22778b4f613e704a5e9150737597178fd4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Dec 2025 15:24:59 +0200 Subject: [PATCH 25/30] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 2 +- .../amplitude/AmplitudeAnalyticsHandler.kt | 2 +- .../firebase/FirebaseAnalyticsHandler.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 2 +- common/routing/detekt-baseline-debug.xml | 9 - .../com/tangem/common/routing/AppRoute.kt | 2 +- .../deeplink/PayloadToDeeplinkConverter.kt | 8 +- data/feedback/detekt-baseline-debug.xml | 7 - .../feedback/DefaultFeedbackRepository.kt | 8 +- .../tangem/data/feedback/di/FeedbackModule.kt | 2 +- data/promo/detekt-baseline-debug.xml | 8 - .../data/promo/DefaultPromoRepository.kt | 7 +- data/transaction/detekt-baseline-debug.xml | 11 - .../DefaultTransactionRepository.kt | 18 +- .../DefaultWalletAddressServiceRepository.kt | 2 +- detekt_baseline_report.txt | 26 +- .../domain/demo/models/DemoConfigCardIds.kt | 397 ++++++++++++++++++ domain/legacy/detekt-baseline-debug.xml | 11 - .../com/tangem/domain/common/LogConfig.kt | 10 +- .../manage-tokens/detekt-baseline-debug.xml | 9 - .../CheckIsCurrencyNotAddedUseCase.kt | 7 +- .../CreateCryptoCurrencyUseCase.kt | 13 +- .../managetokens/ValidateTokenFormUseCase.kt | 1 + .../tangem/domain/promo/PromoRepository.kt | 2 +- .../account/api/detekt-baseline-debug.xml | 8 - .../features/account/PortfolioFetcher.kt | 4 +- .../fetcher/DefaultPortfolioFetcher.kt | 2 +- .../biometry/impl/detekt-baseline-debug.xml | 10 - .../impl/DefaultAskBiometryComponent.kt | 6 +- .../biometry/impl/model/AskBiometryModel.kt | 12 +- .../features/biometry/impl/ui/AskBiometry.kt | 8 +- .../biometry/impl/ui/state/AskBiometryUM.kt | 4 +- .../impl/detekt-baseline-debug.xml | 9 - .../CreateWalletStartModel.kt | 10 +- .../entity/CreateWalletStartUM.kt | 2 +- .../ui/CreateWalletStartContent.kt | 14 +- .../impl/ui/DefaultAddToPortfolioManager.kt | 2 +- .../referral/domain/detekt-baseline-debug.xml | 10 - .../referral/domain/ReferralInteractorImpl.kt | 10 +- .../referral/domain/errors/ReferralError.kt | 6 +- .../onboarding/impl/detekt-baseline-debug.xml | 9 - .../model/TangemPayOnboardingModel.kt | 9 +- .../txhistory/impl/detekt-baseline-debug.xml | 9 - .../txhistory/utils/TxHistoryListManager.kt | 4 +- .../txhistory/utils/TxHistoryListState.kt | 2 +- .../txhistory/utils/TxHistoryUiManager.kt | 12 +- .../model/WalletSettingsModel.kt | 2 +- .../wallet/impl/detekt-baseline-debug.xml | 1 - .../connections/model/WcPairModel.kt | 2 +- 49 files changed, 514 insertions(+), 219 deletions(-) delete mode 100644 common/routing/detekt-baseline-debug.xml delete mode 100644 data/feedback/detekt-baseline-debug.xml delete mode 100644 data/promo/detekt-baseline-debug.xml delete mode 100644 data/transaction/detekt-baseline-debug.xml create mode 100644 domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt delete mode 100644 domain/legacy/detekt-baseline-debug.xml delete mode 100644 domain/manage-tokens/detekt-baseline-debug.xml delete mode 100644 features/account/api/detekt-baseline-debug.xml delete mode 100644 features/biometry/impl/detekt-baseline-debug.xml delete mode 100644 features/create-wallet-start/impl/detekt-baseline-debug.xml delete mode 100644 features/referral/domain/detekt-baseline-debug.xml delete mode 100644 features/tangempay/onboarding/impl/detekt-baseline-debug.xml delete mode 100644 features/txhistory/impl/detekt-baseline-debug.xml diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 7804a24b4f..2815df1738 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index d28504b612..1fdb08a185 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index fbea91a9f4..7e7106ebb4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 57dc3d376e..47c2c3ad6e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, ), diff --git a/common/routing/detekt-baseline-debug.xml b/common/routing/detekt-baseline-debug.xml deleted file mode 100644 index 84f08bf7e3..0000000000 --- a/common/routing/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false - NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) } - NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) } - - diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f1c07144f6..fe5adba5c2 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -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 diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt index ff88b96b22..5bee03bf9b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt @@ -55,8 +55,12 @@ object PayloadToDeeplinkConverter : Converter, 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() } diff --git a/data/feedback/detekt-baseline-debug.xml b/data/feedback/detekt-baseline-debug.xml deleted file mode 100644 index d10388bff4..0000000000 --- a/data/feedback/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultFeedbackRepository.kt$DefaultFeedbackRepository$private val useNewUserWalletsRepository: Boolean - - diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index dcadace780..24ce5d7b07 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -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, @@ -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 diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index 63f1b43180..d6ecc8ef1e 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -42,7 +42,7 @@ internal object FeedbackModule { emailSender = emailSender, appVersionProvider = appVersionProvider, userWalletsListRepository = userWalletsListRepository, - useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, + shouldUseNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, getSelectedWalletUseCase = getSelectedWalletUseCase, ) } diff --git a/data/promo/detekt-baseline-debug.xml b/data/promo/detekt-baseline-debug.xml deleted file mode 100644 index 7ac3d42cb4..0000000000 --- a/data/promo/detekt-baseline-debug.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - SuspendFunSwallowedCancellation:DefaultPromoRepository.kt$DefaultPromoRepository$runCatching - SuspendFunWithFlowReturnType:DefaultPromoRepository.kt$DefaultPromoRepository$suspend - - diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 86c501406d..beb55966c3 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -19,6 +19,7 @@ import com.tangem.domain.promo.models.StoryContent import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -42,7 +43,7 @@ internal class DefaultPromoRepository( .distinctUntilChanged() .map { shouldShow -> when (promoId) { - PromoId.Referral -> runCatching { + PromoId.Referral -> runSuspendCatching { !referralRepository.isReferralParticipant(userWalletId) && shouldShow }.getOrDefault(false) PromoId.Sepa -> { @@ -81,7 +82,7 @@ internal class DefaultPromoRepository( appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false) } - override suspend fun isMarketsStakingNotificationHideClicked(): Flow { + override fun isMarketsStakingNotificationHideClicked(): Flow { return appPreferencesStore.get( key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, default = false, @@ -105,7 +106,7 @@ internal class DefaultPromoRepository( val storedPromo = promoStoriesStore.getSyncOrNull(storyId = id) // Get last stored promo by id if possible or get from network val story = if (storedPromo == null && refresh) { - val storyContent = runCatching { + val storyContent = runSuspendCatching { // Important to return withTimeoutOrNull(STORIES_LOAD_DELAY) { tangemApi.getStoryById(storyId = id).getOrThrow() diff --git a/data/transaction/detekt-baseline-debug.xml b/data/transaction/detekt-baseline-debug.xml deleted file mode 100644 index dd2976b413..0000000000 --- a/data/transaction/detekt-baseline-debug.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - CastNullableToNonNullableType:DefaultTransactionRepository.kt$DefaultTransactionRepository$as - NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$amount - NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$destination - NullableBooleanCheck:DefaultWalletAddressServiceRepository.kt$DefaultWalletAddressServiceRepository$(walletManager as? NearWalletManager)?.validateAddress(address) ?: false - NullableToStringCall:DefaultTransactionRepository.kt$DefaultTransactionRepository$${walletManager?.wallet?.blockchain} - - diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 26cb2d893e..f7c4ffff50 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -38,7 +38,7 @@ import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger -@Suppress("LargeClass") +@Suppress("LargeClass", "NullableToStringCall") internal class DefaultTransactionRepository( private val tangemTechApi: TangemTechApi, private val walletManagersFacade: WalletManagersFacade, @@ -64,12 +64,12 @@ internal class DefaultTransactionRepository( val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo) - val destination = if (amount.type is AmountType.TokenYieldSupply) { + val patchedDestination = if (amount.type is AmountType.TokenYieldSupply) { walletManager.getYieldModuleAddress() } else { destination } - val amount = if (amount.type is AmountType.TokenYieldSupply) { + val patchedAmount = if (amount.type is AmountType.TokenYieldSupply) { amount.copy(value = BigDecimal.ZERO) } else { amount @@ -77,17 +77,17 @@ internal class DefaultTransactionRepository( return@withContext if (fee != null) { walletManager.createTransaction( - amount = amount, + amount = patchedAmount, fee = fee, - destination = destination, + destination = patchedDestination, ).copy( extras = extras, ) } else { TransactionData.Uncompiled( - amount = amount, + amount = patchedAmount, sourceAddress = walletManager.wallet.address, - destinationAddress = destination, + destinationAddress = patchedDestination, extras = extras, fee = null, ) @@ -288,7 +288,7 @@ internal class DefaultTransactionRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) - (walletManager as TransactionSender).send(txData, signer) + (requireNotNull(walletManager) as TransactionSender).send(txData, signer) } override suspend fun sendMultipleTransactions( @@ -304,7 +304,7 @@ internal class DefaultTransactionRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) - (walletManager as TransactionSender).sendMultiple(txsData, signer, sendMode) + (requireNotNull(walletManager) as TransactionSender).sendMultiple(txsData, signer, sendMode) } override fun createTransactionDataExtras( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 23990f9704..49244f2da1 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -89,7 +89,7 @@ class DefaultWalletAddressServiceRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) ?: return@withContext false - (walletManager as? NearWalletManager)?.validateAddress(address) ?: false + (walletManager as? NearWalletManager)?.validateAddress(address) == true } else { blockchain.validateAddress(address) } diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index c9844f8fd5..f0dde73ad3 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-02 18:48:27 +Date: 2025-12-03 22:08:49 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1424 - Modules with Issues: 62 - Average Issues per Module: 22 + Total Issues: 1385 + Modules with Issues: 50 + Average Issues per Module: 27 Progress: - Fixed: 378 out of 1802 (20%) - Remaining: 1424 + Fixed: 417 out of 1802 (23%) + Remaining: 1385 ========================================== All Modules with Issues (sorted by count) @@ -32,7 +32,7 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── features/markets/impl 148 -features/wallet/impl 146 +features/wallet/impl 145 features/onboarding-v2/impl 130 features/swap/impl 67 features/send-v2/impl 57 @@ -76,21 +76,9 @@ libs/visa 5 features/send-v2/api 5 features/home/impl 5 domain/markets 5 -domain/legacy 5 -data/transaction 5 data/account 5 -features/referral/domain 4 -features/biometry/impl 4 data/tokens 4 -features/txhistory/impl 3 -features/tangempay/onboarding/impl 3 -features/create-wallet-start/impl 3 -domain/manage-tokens 3 data/wallet-manager 3 data/manage-tokens 3 -common/routing 3 -features/account/api 2 -data/promo 2 core/config-toggles 2 -data/feedback 1 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt new file mode 100644 index 0000000000..c9a8d5dc7f --- /dev/null +++ b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt @@ -0,0 +1,397 @@ +package com.tangem.domain.demo.models + +@Suppress("LargeClass") +internal object DemoConfigCardIds { + + val releaseDemoCardIds = mutableListOf( + // === Not from the Google Sheet table === + "AC01000000041225", + "AC01000000041472", + "AB01000000046498", + "AB01000000049608", + "AB01000000049574", + "AB01000000046704", + "AB02000000051000", + "AB02000000050911", + + // === Mvideo === + // Wallet + "AC01000000045754", + "AC01000000041662", + "AC01000000041647", + "AC01000000041209", + "AC01000000042462", + "AC01000000041100", + "AC01000000041621", + "AC01000000045960", + "AC01000000041092", + "AC01000000041217", + "AC01000000013489", + "AC01000000028610", + "AC01000000028701", + "AC01000000028578", + "AC01000000027281", + "AC01000000027216", + "AC01000000028594", + "AC01000000028602", + "AC01000000028636", + "AC01000000013968", + "AC01000000027208", + "AC01000000013471", + "AC01000000028586", + "AC01000000013703", + "AC01000000028628", + "AC01000000028693", + "AC01000000028685", + "AC01000000013950", + "AC01000000013828", + "AC01000000013497", + "AC01000000013836", + "AC01000000013505", + "AC03000000046693", + "AC03000000046685", + "AC03000000046677", + "AC03000000046669", + "AC03000000046651", + "AC03000000046644", + "AC03000000046636", + "AC03000000046628", + "AC03000000046610", + "AC03000000046602", + "AC03000000046594", + "AC03000000046586", + "AC03000000046578", + "AC03000000046560", + "AC03000000046552", + "AC03000000046545", + "AC03000000046537", + "AC03000000046529", + "AC03000000046511", + "AC03000000046800", + "AC03000000046792", + "AC03000000046784", + "AC03000000046776", + "AC03000000046768", + "AC03000000046750", + "AC03000000046743", + "AC03000000046735", + "AC03000000046727", + "AC03000000046446", + "AC03000000046438", + "AC03000000046412", + "AC03000000046388", + "AC03000000046370", + "AC03000000046354", + "AC03000000046347", + "AC03000000046339", + "AC03000000046321", + "AC03000000046172", + "AC03000000046396", + "AC03000000046404", + "AC03000000046701", + "AC03000000046420", + "AC03000000046719", + "AC03000000046503", + "AC03000000046495", + "AC03000000046487", + "AC03000000046362", + "AC03000000046479", + "AC03000000046461", + "AC03000000046453", + + // Note BTC + "AB01000000059608", + "AB01000000046647", + "AB01000000046571", + "AB01000000046746", + "AB01000000059574", + "AB01000000046753", + "AB01000000046605", + "AB01000000046761", + "AB01000000046720", + "AB01000000046530", + "AB01000000016475", + "AB01000000016483", + "AB01000000016491", + "AB01000000020709", + "AB01000000020717", + "AB01000000015550", + "AB01000000015394", + "AB01000000016079", + "AB01000000016087", + "AB01000000016095", + "AB01000000020915", + "AB01000000017184", + "AB01000000020907", + "AB01000000017192", + "AB01000000016210", + "AB01000000016111", + "AB01000000016103", + "AB01000000015766", + "AB01000000015774", + "AB01000000015782", + "AB01000000022598", + "AB01000000022580", + "AB01000000005688", + "AB07000000005696", + "AB07000000005902", + "AB07000000005910", + "AB07000000005928", + "AB07000000005936", + "AB07000000005944", + "AB07000000005993", + "AB07000000005985", + "AB07000000005977", + "AB07000000005969", + "AB07000000005951", + "AB07000000005605", + "AB07000000005803", + "AB07000000005811", + "AB07000000005829", + "AB07000000005837", + "AB07000000005845", + "AB07000000005852", + "AB07000000005860", + "AB07000000005878", + "AB07000000005886", + "AB07000000005894", + "AB07000000005704", + "AB07000000005712", + "AB07000000005720", + "AB07000000005738", + "AB07000000005746", + "AB07000000005514", + "AB07000000005522", + "AB07000000005563", + "AB07000000005571", + "AB07000000005589", + "AB07000000005597", + "AB07000000005613", + "AB07000000005621", + "AB07000000005639", + "AB07000000005647", + "AB07000000005654", + "AB07000000005662", + "AB07000000005670", + "AB07000000005530", + "AB07000000005548", + "AB07000000005555", + "AB07000000005753", + "AB07000000005761", + "AB07000000005779", + "AB07000000005787", + "AB07000000005795", + "AB07000000005506", + + // Note ETH + "AB02000000051083", + "AB02000000051059", + "AB02000000051158", + "AB02000000050986", + "AB02000000051026", + "AB02000000050960", + "AB02000000051042", + "AB02000000051091", + "AB02000000051034", + "AB02000000051133", + "AB02000000019924", + "AB02000000019932", + "AB02000000022092", + "AB02000000022282", + "AB02000000023983", + "AB02000000023439", + "AB02000000020328", + "AB02000000020310", + "AB02000000021565", + "AB02000000022357", + "AB02000000023355", + "AB02000000022324", + "AB02000000022100", + "AB02000000019999", + "AB02000000020013", + "AB02000000020005", + "AB02000000020021", + "AB02000000020039", + "AB02000000020278", + "AB02000000020252", + "AB02000000018652", + "AB02000000018561", + "AB08000000009481", + "AB08000000009473", + "AB08000000009705", + "AB08000000009897", + "AB08000000009689", + "AB08000000009671", + "AB08000000009465", + "AB08000000009457", + "AB08000000009440", + "AB08000000009432", + "AB08000000009424", + "AB08000000009416", + "AB08000000009408", + "AB08000000009390", + "AB08000000009374", + "AB08000000009382", + "AB08000000009267", + "AB08000000009275", + "AB08000000009283", + "AB08000000009291", + "AB08000000009309", + "AB08000000009317", + "AB08000000009325", + "AB08000000009333", + "AB08000000009341", + "AB08000000009358", + "AB08000000009366", + "AB08000000009077", + "AB08000000009143", + "AB08000000009168", + "AB08000000009184", + "AB08000000009192", + "AB08000000009200", + "AB08000000009226", + "AB08000000009218", + "AB08000000009234", + "AB08000000009242", + "AB08000000008574", + "AB08000000009069", + "AB08000000008525", + "AB08000000009051", + "AB08000000009135", + "AB08000000009150", + "AB08000000009176", + "AB08000000009085", + "AB08000000009093", + "AB08000000009101", + "AB08000000009119", + "AB08000000009127", + "AB08000000009259", + + // === Technopark === + // Wallet + "AC01000000044120", + "AC01000000044997", + "AC01000000044989", + "AC01000000043494", + "AC01000000043486", + "AC01000000044187", + "AC01000000043148", + "AC01000000044013", + "AC01000000043973", + "AC01000000044815", + "AC01000000044807", + "AC01000000043809", + "AC01000000043833", + "AC01000000043460", + "AC01000000043064", + "AC01000000044138", + "AC01000000044500", + "AC01000000044492", + "AC01000000044260", + "AC01000000044278", + + // Note BTC + "AB01000000049864", + "AB01000000053239", + "AB01000000053056", + "AB01000000054237", + "AB01000000054245", + "AB01000000054211", + "AB01000000054229", + "AB01000000053189", + "AB01000000054195", + "AB01000000050797", + "AB01000000053833", + "AB01000000052124", + "AB01000000051605", + "AB01000000052223", + "AB01000000052207", + "AB01000000052199", + "AB01000000047785", + "AB01000000047850", + "AB01000000047868", + "AB01000000048288", + + // Note ETH + "AB02000000049715", + "AB02000000049848", + "AB02000000049814", + "AB02000000049863", + "AB02000000049871", + "AB02000000049855", + "AB02000000049285", + "AB02000000049277", + "AB02000000049558", + "AB02000000049889", + "AB02000000049988", + "AB02000000049707", + "AB02000000049699", + "AB02000000049897", + "AB02000000049905", + "AB02000000049913", + "AB02000000049251", + "AB02000000049533", + "AB02000000049541", + "AB02000000049830", + // === more cids === + "AC03000000091418", + "AC03000000091400", + "AC03000000099007", + "AC03000000098991", + "AC03000000098942", + "AC03000000091715", + "AC03000000091301", + "AC03000000091343", + "AB01000000055705", + "AB01000000052918", + "AB01000000047710", + "AB01000000052306", + "AB01000000047645", + "AB01000000048957", + "AB01000000052900", + "AB01000000050391", + "AB01000000047363", + "AB02000000053998", + "AB02000000019809", + "AB02000000020872", + "AB02000000022027", + "AB02000000058955", + "AB02000000053253", + "AB02000000048063", + "AB02000000023736", + "AB02000000058187", + "AB02000000000007", + "AC03000000076229", + "AF04000000000118", + + // Wallet 2 + "AF04000000012006", + "AF04000000012014", + "AF04000000012022", + "AF04000000012030", + "AF15000000257889", + "AF15000000257897", + "AF15000000637809", + "AF15000000640282", + "AF15000001187424", + "AF15000001187408", + "AF15000001187416", + "AF15000001195781", + "AF15000001195773", + "AF15000001195799", + // Wallet 2 QA + "AF12345678912346", + "AF12345678912361", + "AF10100000000076", + "AF10100000000084", + ) + + val testDemoCardIds = listOf( + "FB20000000000186", // Note ETH + "FB10000000000196", // Note BTC + "FB30000000000176", // Wallet + "FB04000000000152", // Wallet 2 + ) + + val debugTestDemoCardIds = emptyList() +} \ No newline at end of file diff --git a/domain/legacy/detekt-baseline-debug.xml b/domain/legacy/detekt-baseline-debug.xml deleted file mode 100644 index 3d02fabec6..0000000000 --- a/domain/legacy/detekt-baseline-debug.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val amplitude: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val appsflyer: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val firebase: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$LogConfig$val storeAction: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$NetworkLogConfig$val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED - - diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 3adab018df..1a8068cfb5 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -4,17 +4,17 @@ import com.tangem.domain.features.BuildConfig object LogConfig { const val imageLoader: Boolean = false - val storeAction: Boolean = BuildConfig.LOG_ENABLED + val shouldStoreAction: Boolean = BuildConfig.LOG_ENABLED val network: NetworkLogConfig = NetworkLogConfig val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig } object NetworkLogConfig { - val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED + val isBlockchainSdkNetworkLogEnabled: Boolean = BuildConfig.LOG_ENABLED } object AnalyticsHandlersLogConfig { - val firebase: Boolean = BuildConfig.LOG_ENABLED - val amplitude: Boolean = BuildConfig.LOG_ENABLED - val appsflyer: Boolean = BuildConfig.LOG_ENABLED + val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/manage-tokens/detekt-baseline-debug.xml b/domain/manage-tokens/detekt-baseline-debug.xml deleted file mode 100644 index 5c9a9d5ec2..0000000000 --- a/domain/manage-tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - NamedArguments:CheckIsCurrencyNotAddedUseCase.kt$CheckIsCurrencyNotAddedUseCase$isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) - NamedArguments:CreateCryptoCurrencyUseCase.kt$CreateCryptoCurrencyUseCase$createCustomToken(userWalletId, networkId, derivationPath, formValues) - NamedArguments:ValidateTokenFormUseCase.kt$ValidateTokenFormUseCase$zipOrAccumulate( { ensureIsContractAddressValid(formValues.contractAddress, networkId) }, { ensureIsDecimalsValid(formValues.decimals) }, { ensure(formValues.name.isNotBlank()) { CustomTokenFormValidationException.EmptyName } }, { ensure(formValues.symbol.isNotBlank()) { CustomTokenFormValidationException.EmptySymbol } }, ) { contractAddress, decimals, _, _ -> AddCustomTokenForm.Validated.All( contractAddress = contractAddress, symbol = formValues.symbol, name = formValues.name, decimals = decimals, ) } - - diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt index e6bd36e0a3..51b7543912 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt @@ -15,6 +15,11 @@ class CheckIsCurrencyNotAddedUseCase( derivationPath: Network.DerivationPath, contractAddress: String?, ): Either = Either.catch { - repository.isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) + repository.isCurrencyNotAdded( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + contractAddress = contractAddress, + ) } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt index 1b305b4812..359c4011fb 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt @@ -57,9 +57,18 @@ class CreateCryptoCurrencyUseCase( formValues: AddCustomTokenForm.Validated.All?, ): Either = Either.catch { if (formValues == null) { - customTokensRepository.createCoin(userWalletId, networkId, derivationPath) + customTokensRepository.createCoin( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + ) } else { - customTokensRepository.createCustomToken(userWalletId, networkId, derivationPath, formValues) + customTokensRepository.createCustomToken( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + formValues = formValues, + ) } } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt index d909ca4c6a..30ec3baf72 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt @@ -12,6 +12,7 @@ class ValidateTokenFormUseCase( private val repository: CustomTokensRepository, ) { + @Suppress("NamedArguments") suspend operator fun invoke( networkId: Network.ID, formValues: AddCustomTokenForm.Raw, diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 18d7a50ec4..d8ea76a512 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -16,7 +16,7 @@ interface PromoRepository { suspend fun setNeverToShowTokenPromo(promoId: PromoId) - suspend fun isMarketsStakingNotificationHideClicked(): Flow + fun isMarketsStakingNotificationHideClicked(): Flow suspend fun setMarketsStakingNotificationHideClicked() // endregion diff --git a/features/account/api/detekt-baseline-debug.xml b/features/account/api/detekt-baseline-debug.xml deleted file mode 100644 index 1e04f9d55a..0000000000 --- a/features/account/api/detekt-baseline-debug.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean - UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten() - - diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt index 1884981a50..a56d06a5dd 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt @@ -26,7 +26,7 @@ interface PortfolioFetcher { val isSingleChoice: Boolean = balances.values .map { it.accountsBalance.accountStatuses } - .flatten().size == 1 + .sumOf { it.size } == 1 fun isSingleChoice(walletId: UserWalletId): Boolean = balances[walletId] ?.accountsBalance @@ -43,7 +43,7 @@ interface PortfolioFetcher { } sealed interface Mode { - data class All(val onlyMultiCurrency: Boolean) : Mode + data class All(val isOnlyMultiCurrency: Boolean) : Mode data class Wallet(val walletId: UserWalletId) : Mode } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt index fcdd27ea9d..f8bf368a40 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt @@ -72,7 +72,7 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor( private fun List.filterWallets(mode: Mode): List = this.filter { wallet -> when (mode) { - is Mode.All -> if (mode.onlyMultiCurrency) wallet.isMultiCurrency else true + is Mode.All -> if (mode.isOnlyMultiCurrency) wallet.isMultiCurrency else true is Mode.Wallet -> wallet.walletId == mode.walletId } } diff --git a/features/biometry/impl/detekt-baseline-debug.xml b/features/biometry/impl/detekt-baseline-debug.xml deleted file mode 100644 index f439e02f2e..0000000000 --- a/features/biometry/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val bottomSheetVariant: Boolean = false - BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val showProgress: Boolean = false - BooleanPropertyNaming:DefaultAskBiometryComponent.kt$DefaultAskBiometryComponent$val bsShown by bsShown.collectAsStateWithLifecycle() - MultilineLambdaItParameter:AskBiometryModel.kt$AskBiometryModel${ uiMessageSender.send( SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), ) } - - diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt index 81ca241043..4fb9e79cd1 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt @@ -41,10 +41,10 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor( @Composable override fun BottomSheet() { val state by model.uiState.collectAsStateWithLifecycle() - val bsShown by bsShown.collectAsStateWithLifecycle() - val bsConfig = remember(this, bsShown) { + val isBSShown by bsShown.collectAsStateWithLifecycle() + val bsConfig = remember(this, isBSShown) { TangemBottomSheetConfig( - isShown = bsShown, + isShown = isBSShown, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index b6f43fdbbe..2d586fad73 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -55,7 +55,7 @@ internal class AskBiometryModel @Inject constructor( private val _uiState = MutableStateFlow( AskBiometryUM( - bottomSheetVariant = params.isBottomSheetVariant, + isBottomSheetVariant = params.isBottomSheetVariant, onAllowClick = ::onAllowClick, onDontAllowClick = ::dontAllow, onDismiss = ::dismiss, @@ -85,7 +85,7 @@ internal class AskBiometryModel @Inject constructor( return@launch } - _uiState.update { it.copy(showProgress = true) } + _uiState.update { it.copy(shouldShowProgress = true) } /* @@ -96,7 +96,7 @@ internal class AskBiometryModel @Inject constructor( uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), ) - _uiState.update { it.copy(showProgress = false) } + _uiState.update { it.copy(shouldShowProgress = false) } return@launch } @@ -128,7 +128,7 @@ internal class AskBiometryModel @Inject constructor( } } - if (_uiState.value.bottomSheetVariant) { + if (_uiState.value.isBottomSheetVariant) { dismissBSFlow.emit(Unit) delay(timeMillis = 500) } @@ -142,9 +142,9 @@ internal class AskBiometryModel @Inject constructor( userWalletId = userWallet.walletId, lockMethod = UserWalletsListRepository.LockMethod.Biometric, changeUnsecured = false, - ).onLeft { + ).onLeft { error -> uiMessageSender.send( - SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), + SnackbarMessage(stringReference("Something went wrong. Please contact support: $error")), ) } } diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt index cd367b9850..a3726bf381 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt @@ -29,7 +29,7 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) { Column( modifier = Modifier.weight(1f), ) { - if (state.bottomSheetVariant) { + if (state.isBottomSheetVariant) { Header(onCloseClick = state.onDismiss) } @@ -128,12 +128,12 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) { ) { PrimaryButton( modifier = Modifier.fillMaxWidth(), - showProgress = state.showProgress, + showProgress = state.shouldShowProgress, text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics), onClick = state.onAllowClick, ) - if (state.bottomSheetVariant.not()) { + if (state.isBottomSheetVariant.not()) { SpacerH12() SecondaryButton( @@ -199,7 +199,7 @@ private fun Preview() { private fun PreviewBS() { TangemThemePreview { AskBiometry( - state = AskBiometryUM(bottomSheetVariant = true), + state = AskBiometryUM(isBottomSheetVariant = true), ) } } \ No newline at end of file diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt index 0148aba873..88ed6c696b 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt @@ -3,8 +3,8 @@ package com.tangem.features.biometry.impl.ui.state import com.tangem.core.ui.extensions.TextReference internal data class AskBiometryUM( - val bottomSheetVariant: Boolean = false, - val showProgress: Boolean = false, + val isBottomSheetVariant: Boolean = false, + val shouldShowProgress: Boolean = false, val error: TextReference? = null, val onAllowClick: () -> Unit = {}, val onDontAllowClick: () -> Unit = {}, diff --git a/features/create-wallet-start/impl/detekt-baseline-debug.xml b/features/create-wallet-start/impl/detekt-baseline-debug.xml deleted file mode 100644 index 7090dd6ebf..0000000000 --- a/features/create-wallet-start/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:CreateWalletStartUM.kt$CreateWalletStartUM$val showScanSecondaryButton: Boolean - MultilineLambdaItParameter:CreateWalletStartContent.kt${ FeatureItem( iconResId = it.iconResId, text = it.text, ) } - MultilineLambdaItParameter:CreateWalletStartModel.kt$CreateWalletStartModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } } - - diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index c0051682a8..4d1739bbe3 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -84,7 +84,7 @@ internal class CreateWalletStartModel @Inject constructor( ), ), imageResId = R.drawable.img_hardware_wallet, - showScanSecondaryButton = true, + shouldShowScanSecondaryButton = true, onPrimaryButtonClick = ::onBuyClick, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), @@ -112,7 +112,7 @@ internal class CreateWalletStartModel @Inject constructor( ), ), imageResId = R.drawable.img_mobile_wallet, - showScanSecondaryButton = false, + shouldShowScanSecondaryButton = false, onPrimaryButtonClick = ::onStartWithMobileWalletClick, primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), @@ -182,11 +182,11 @@ internal class CreateWalletStartModel @Inject constructor( } saveWalletUseCase(userWallet = userWallet).fold( - ifLeft = { + ifLeft = { error -> delay(HIDE_PROGRESS_DELAY) setLoading(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt index 58f58d2bc0..b45777a20a 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt @@ -9,7 +9,7 @@ internal data class CreateWalletStartUM( val featureItems: ImmutableList, val imageResId: Int, val isScanInProgress: Boolean, - val showScanSecondaryButton: Boolean, + val shouldShowScanSecondaryButton: Boolean, val primaryButtonText: TextReference, val onPrimaryButtonClick: () -> Unit, val otherMethodDescription: TextReference, diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt index 64070ed61e..ecf741b0f0 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt @@ -120,10 +120,10 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.spacedBy(8.dp), ) { - state.featureItems.forEach { + state.featureItems.forEach { item -> FeatureItem( - iconResId = it.iconResId, - text = it.text, + iconResId = item.iconResId, + text = item.text, ) } } @@ -143,7 +143,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi ) }, bottomContent = { - if (state.showScanSecondaryButton) { + if (state.shouldShowScanSecondaryButton) { SecondaryButtonIconEnd( modifier = Modifier .fillMaxWidth() @@ -232,7 +232,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi minImageHeight = 160.dp, ) } - if (!state.showScanSecondaryButton) { + if (!state.shouldShowScanSecondaryButton) { FlowRow( modifier = Modifier .wrapContentWidth() @@ -425,7 +425,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide ), ), imageResId = R.drawable.img_hardware_wallet, - showScanSecondaryButton = true, + shouldShowScanSecondaryButton = true, onPrimaryButtonClick = { }, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), @@ -455,7 +455,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide ), ), imageResId = R.drawable.img_mobile_wallet, - showScanSecondaryButton = false, + shouldShowScanSecondaryButton = false, onPrimaryButtonClick = { }, primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt index c68c7b7ad5..9c82131fe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt @@ -31,7 +31,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor( override val allAvailableNetworks: Flow> = _allAvailableNetworks.asSharedFlow() override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true), + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), scope = scope, ) diff --git a/features/referral/domain/detekt-baseline-debug.xml b/features/referral/domain/detekt-baseline-debug.xml deleted file mode 100644 index 822bbfee9f..0000000000 --- a/features/referral/domain/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - MultilineLambdaItParameter:ReferralInteractorImpl.kt$ReferralInteractorImpl${ Timber.e("Failed to derive public keys: $it") throw it.mapToDomainError() } - ObjectExtendsThrowable:ReferralError.kt$ReferralError$SdkError : ReferralError - ObjectExtendsThrowable:ReferralError.kt$ReferralError$UserCancelledException : ReferralError - UselessCallOnNotNull:ReferralInteractorImpl.kt$ReferralInteractorImpl$listOfNotNull(cryptoCurrency) - - diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 78d092c2da..02ddfaaf01 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -72,9 +72,9 @@ internal class ReferralInteractorImpl( manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) } is PortfolioId.Wallet -> { - derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { - Timber.e("Failed to derive public keys: $it") - throw it.mapToDomainError() + derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> + Timber.e("Failed to derive public keys: $throwable") + throw throwable.mapToDomainError() } addCryptoCurrenciesUseCase( @@ -118,9 +118,9 @@ internal class ReferralInteractorImpl( private fun Throwable.mapToDomainError(): ReferralError { if (this !is TangemSdkError) return ReferralError.DataError(this) return if (this is TangemSdkError.UserCancelled) { - ReferralError.UserCancelledException + ReferralError.UserCancelledException() } else { - ReferralError.SdkError + ReferralError.SdkError() } } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt index 8d78d31fdd..05e5eac920 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt @@ -1,8 +1,8 @@ package com.tangem.feature.referral.domain.errors sealed class ReferralError : Exception() { - data object UserCancelledException : ReferralError() - data object SdkError : ReferralError() + class UserCancelledException : ReferralError() + class SdkError : ReferralError() - data class DataError(val throwable: Throwable) : ReferralError() + class DataError(val throwable: Throwable) : ReferralError() } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/detekt-baseline-debug.xml b/features/tangempay/onboarding/impl/detekt-baseline-debug.xml deleted file mode 100644 index 244ca0e5f5..0000000000 --- a/features/tangempay/onboarding/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - MultilineLambdaItParameter:TangemPayOnboardingModel.kt$TangemPayOnboardingModel${ TangemPayOnboardingScreenState.Content( onBack = it.onBack, onTermsClick = ::onTermsClick, buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig( isLoading = false, onClick = ::onGetCardClick, ), ) } - MultilineLambdaItParameter:TangemPayOnboardingModel.kt$TangemPayOnboardingModel${ Timber.e("Error getCustomerInfo: ${it.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) } - NullableToStringCall:TangemPayOnboardingModel.kt$TangemPayOnboardingModel$${result.leftOrNull()?.message} - - diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 688336b8ca..4947048c06 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -56,9 +56,9 @@ internal class TangemPayOnboardingModel @Inject constructor( } private fun showOnboarding() { - uiState.update { + uiState.update { state -> TangemPayOnboardingScreenState.Content( - onBack = it.onBack, + onBack = state.onBack, onTermsClick = ::onTermsClick, buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig( isLoading = false, @@ -92,6 +92,7 @@ internal class TangemPayOnboardingModel @Inject constructor( urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } + @Suppress("NullableToStringCall") private fun onGetCardClick() { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) modelScope.launch { @@ -108,8 +109,8 @@ internal class TangemPayOnboardingModel @Inject constructor( repository.getCustomerInfo( userWalletId = userWalletId, ).fold( - ifLeft = { - Timber.e("Error getCustomerInfo: ${it.errorCode}") + ifLeft = { error -> + Timber.e("Error getCustomerInfo: ${error.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) }, ifRight = { customerInfo -> diff --git a/features/txhistory/impl/detekt-baseline-debug.xml b/features/txhistory/impl/detekt-baseline-debug.xml deleted file mode 100644 index 7a98042e54..0000000000 --- a/features/txhistory/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:TxHistoryListManager.kt$TxHistoryListManager$val clearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating - MultilineLambdaItParameter:TxHistoryUiManager.kt$TxHistoryUiManager${ it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading && it.status !is PaginationStatus.InitialLoadingError } - UseEmptyCounterpart:TxHistoryListState.kt$TxHistoryListState$listOf() - - diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt index 1c8153f1cb..485c064e0a 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt @@ -88,13 +88,13 @@ internal class TxHistoryListManager( private fun updateState(batchListState: BatchListState>) { state.update { state -> - val clearUiBatches = + val shouldClearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating state.copy( status = batchListState.status, uiBatches = uiManager.createOrUpdateUiBatches( newCurrencyBatches = batchListState.data, - clearUiBatches = clearUiBatches, + shouldClearUiBatches = shouldClearUiBatches, ), ) } diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt index 8e553fe6ac..e27adfb4ce 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt @@ -6,5 +6,5 @@ import com.tangem.pagination.PaginationStatus data class TxHistoryListState( val status: PaginationStatus<*> = PaginationStatus.None, - val uiBatches: List>> = listOf(), + val uiBatches: List>> = emptyList(), ) \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt index 3bf17cde84..84a2b5be73 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt @@ -22,10 +22,10 @@ internal class TxHistoryUiManager( @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = state // filter initial states, since we dont emit loading items as UI items - .filter { - it.status !is PaginationStatus.None && - it.status !is PaginationStatus.InitialLoading && - it.status !is PaginationStatus.InitialLoadingError + .filter { state -> + state.status !is PaginationStatus.None && + state.status !is PaginationStatus.InitialLoading && + state.status !is PaginationStatus.InitialLoadingError } .mapLatest { state -> state.uiBatches.asSequence() @@ -36,10 +36,10 @@ internal class TxHistoryUiManager( fun createOrUpdateUiBatches( newCurrencyBatches: List>>, - clearUiBatches: Boolean, + shouldClearUiBatches: Boolean, ): List>> { val currentUiBatches = state.value.uiBatches - val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList() + val batches = if (shouldClearUiBatches) mutableListOf() else currentUiBatches.toMutableList() for ((key, data) in newCurrencyBatches) { // Find if batch with same key exists diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 826d89ac75..a44d3ead1c 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -438,7 +438,7 @@ internal class WalletSettingsModel @Inject constructor( AppRoute.CreateWalletBackup( userWalletId = params.userWalletId, isUpgradeFlow = isUpgradeFlow, - setAccessCode = true, + shouldSetAccessCode = true, analyticsSource = AnalyticsParam.ScreensSources.WalletSettings.value, analyticsAction = if (isUpgradeFlow) { RecoveryPhraseScreenAction.Backup.value diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index c83e37ad72..5a9fa2634b 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -71,7 +71,6 @@ MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ it .conflate() .distinctUntilChanged() .onEach { selectedWallet -> if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index aaf93e1932..76072cedae 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -118,7 +118,7 @@ internal class WcPairModel @Inject constructor( init { if (accountsFeatureToggles.isFeatureEnabled) { portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true), + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), scope = modelScope, ) modelScope.launch { From bc174f2f061932d5ba424911aad208b11374997d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 15:25:35 +0700 Subject: [PATCH 26/30] Updated on 2026-08-14 --- .../createedit/ui/AccountCreateEditContent.kt | 10 +++------- .../lib/crypto/derivation/AccountNodeRecognizer.kt | 13 ++++++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 4828371761..5d48231c07 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -1,15 +1,10 @@ package com.tangem.features.account.createedit.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -188,7 +183,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { modifier = Modifier .fillMaxWidth() .padding(contentPadding), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), ) { colorsState.list.fastForEach { color -> val isSelected = color == colorsState.selected @@ -238,6 +233,7 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { FlowRow( maxItemsInEachRow = 6, modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, ) { iconsState.list.fastForEachIndexed { index, icon -> val isSelected = icon == iconsState.selected diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 82780c4813..328bf4053e 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -28,7 +28,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { // region Ethereum-like blockchains blockchain == Blockchain.Tezos -> ACCOUNT_NODE_INDEX blockchain == Blockchain.Quai -> ACCOUNT_NODE_INDEX - blockchain.isEvm() -> ADDRESS_INDEX_NODE_INDEX + blockchain.isEvm() && derivationPath.isEthDerivation() -> ADDRESS_INDEX_NODE_INDEX // endregion blockchain.isUTXO -> ACCOUNT_NODE_INDEX else -> ACCOUNT_NODE_INDEX @@ -75,6 +75,13 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { .getOrNull() } + private fun DerivationPath.isEthDerivation(): Boolean { + fun getIndex(nodeIndex: Int) = nodes.getOrNull(nodeIndex)?.getIndex(false) + + return getIndex(FIRST_NODE_INDEX) == ETH_FIRST_NODE_VALUE && + getIndex(SECOND_NODE_INDEX) == ETH_SECOND_NODE_VALUE + } + @Suppress("LongMethod") private fun Blockchain.isAccountsSupported(): Boolean { return when (this) { @@ -253,5 +260,9 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { private companion object { const val ACCOUNT_NODE_INDEX = 2 const val ADDRESS_INDEX_NODE_INDEX = 4 + const val FIRST_NODE_INDEX = 0 + const val ETH_FIRST_NODE_VALUE = 44L + const val SECOND_NODE_INDEX = 1 + const val ETH_SECOND_NODE_VALUE = 60L } } \ No newline at end of file From d16bca1c5b5e5945686361020784ca7cf4f29f63 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 13:49:17 +0500 Subject: [PATCH 27/30] Updated on 2026-08-14 --- .../analytics/DefaultTrackingContextProxy.kt | 8 ++- .../CardContextInterceptor.kt | 1 + .../HotWalletContextInterceptor.kt | 3 ++ .../di/UserWalletsListManagerModule.kt | 5 ++ .../DefaultUserWalletsListRepository.kt | 30 ++++++++++- .../welcome/redux/WelcomeMiddleware.kt | 10 ++-- .../component/impl/DefaultRoutingComponent.kt | 23 +++++++++ .../core/analytics/models/AnalyticsParam.kt | 4 +- .../com/tangem/core/analytics/models/Basic.kt | 51 ++++++++++++++----- .../core/analytics/models/event/SignIn.kt | 46 +++++++++++++++++ .../analytics/utils/TrackingContextProxy.kt | 2 + .../card/analytics/IntroductionProcess.kt | 12 ++++- .../CreateWalletSelectionModel.kt | 7 ++- .../CreateWalletStartModel.kt | 29 +++-------- .../features/details/model/DetailsModel.kt | 19 +++++-- .../features/home/impl/model/HomeModel.kt | 8 +-- .../CreateHardwareWalletModel.kt | 27 +++------- .../upgradewallet/UpgradeWalletModel.kt | 5 ++ .../walletbackup/model/WalletBackupModel.kt | 2 + .../model/WalletHardwareBackupModel.kt | 2 + .../welcome/impl/model/WelcomeModel.kt | 32 ++++++++++++ 21 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt index a32012ca51..923b26cbf5 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 1608ebc3f4..c7bc17f8e9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt index b7be0155f7..ab9c9d3b54 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt @@ -14,6 +14,9 @@ class HotWalletContextInterceptor( override fun intercept(params: MutableMap) { params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value + params.remove(AnalyticsParam.BATCH) + params.remove(AnalyticsParam.FIRMWARE) + params.remove(AnalyticsParam.CURRENCY) } companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index e0b08af719..abe6b49ded 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index af0ce672e0..29f19ffb75 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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?>(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, + ), + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index f912f0948a..3fea04772f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 7025664f0b..483891f435 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -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, + ), + ) + } + } + } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 47d586e92e..b0114af14c 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 9d2974a077..c59513d2fa 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -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, ), ) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt new file mode 100644 index 0000000000..5d0e483b1c --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt @@ -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 = 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, + ), + ) +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt b/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt index 23be99e1fd..30671f81d0 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/utils/TrackingContextProxy.kt @@ -23,4 +23,6 @@ interface TrackingContextProxy { fun addHotWalletContext() fun removeContext() + + fun proceedWithContext(userWallet: UserWallet, action: () -> Unit) } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 15916c16cf..06b18ef403 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam sealed class IntroductionProcess( event: String, @@ -10,5 +11,14 @@ sealed class IntroductionProcess( object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") object ButtonTokensList : IntroductionProcess("Button - Tokens List") object ButtonBuyCards : IntroductionProcess("Button - Buy Cards") - object ButtonScanCard : IntroductionProcess("Button - Scan Card") + object ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") + + class ButtonScanCard( + val source: AnalyticsParam.ScreensSources, + ) : IntroductionProcess( + event = "Button - Scan Card", + params = mapOf( + AnalyticsParam.Key.SOURCE to source.value, + ), + ) } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index bc586d7d95..7e9fcbc7f5 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -2,6 +2,8 @@ package com.tangem.features.createwalletselection import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -9,8 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.card.analytics.IntroductionProcess -import com.tangem.domain.card.analytics.Shop import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.features.createwalletselection.impl.R @@ -101,8 +101,7 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onBuyClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) - analyticsEventHandler.send(Shop.ScreenOpened) + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index 4d1739bbe3..6e138c594d 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -6,8 +6,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -19,8 +18,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.analytics.ParamCardCurrencyConverter -import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError @@ -51,7 +49,6 @@ internal class CreateWalletStartModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val analyticsEventHandler: AnalyticsEventHandler, private val appRouter: AppRouter, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, @@ -59,6 +56,7 @@ internal class CreateWalletStartModel @Inject constructor( @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -126,6 +124,9 @@ internal class CreateWalletStartModel @Inject constructor( ) private fun onScanClick() { + analyticsEventHandler.send( + event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateNewWallet), + ) scanCard() } @@ -134,6 +135,7 @@ internal class CreateWalletStartModel @Inject constructor( } private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateNewWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } @@ -199,28 +201,11 @@ internal class CreateWalletStartModel @Inject constructor( }, ifRight = { setLoading(false) - sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported) appRouter.replaceAll(AppRoute.Wallet) }, ) } - private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - if (currency != null) { - analyticsEventHandler.send( - SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = SignInType.Card, - walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), - isImported = isImported, - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } - private fun setLoading(isLoading: Boolean) { uiState.update { it.copy(isScanInProgress = isLoading) } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index cf7047bb68..be5d74ca8e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -4,6 +4,9 @@ import android.content.res.Resources import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.AppInstanceIdProvider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -13,13 +16,14 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.repository.FeedbackFeatureToggles +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.component.DetailsComponent @@ -29,6 +33,7 @@ import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS import com.tangem.features.details.utils.ItemsBuilder import com.tangem.features.details.utils.SocialsBuilder +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import kotlinx.collections.immutable.ImmutableList @@ -60,6 +65,9 @@ internal class DetailsModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val feedbackFeatureToggles: FeedbackFeatureToggles, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: DetailsComponent.Params = paramsContainer.require() @@ -216,7 +224,12 @@ internal class DetailsModel @Inject constructor( private fun onBuyClick() { modelScope.launch { - urlOpener.openUrl(buildBuyLink()) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings)) + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } else { + urlOpener.openUrl(buildBuyLink()) + } } } diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index f82b96124a..a02c3c4543 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -8,8 +8,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic.SignedInLegacy +import com.tangem.core.analytics.models.Basic.SignedInLegacy.SignInType import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -127,7 +127,7 @@ internal class HomeModel @Inject constructor( } private fun onScanClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy) scanCard() } @@ -212,7 +212,7 @@ internal class HomeModel @Inject constructor( val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) if (currency != null) { analyticsEventHandler.send( - SignedIn( + SignedInLegacy( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index da08951a67..fabdc48487 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -5,8 +5,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -16,8 +15,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.analytics.ParamCardCurrencyConverter -import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError @@ -74,12 +72,16 @@ internal class CreateHardwareWalletModel @Inject constructor( } private fun onBuyTangemWalletClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onScanDeviceClick() { + analyticsEventHandler.send( + event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWallet), + ) scanCard() } @@ -143,28 +145,11 @@ internal class CreateHardwareWalletModel @Inject constructor( }, ifRight = { setLoading(false) - sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported) router.replaceAll(AppRoute.Wallet) }, ) } - private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - if (currency != null) { - analyticsEventHandler.send( - SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = SignInType.Card, - walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), - isImported = isImported, - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } - private fun setLoading(isLoading: Boolean) { uiState.update { it.copy(isScanInProgress = isLoading) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index de7cbe111f..dcaa94647a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -7,6 +7,8 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -18,6 +20,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -85,12 +88,14 @@ internal class UpgradeWalletModel @Inject constructor( } private fun onBuyTangemWalletClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onContinueClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.Upgrade)) analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade) scanCard() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 1e183bf849..69ed17c12f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.walletbackup.model import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -111,6 +112,7 @@ internal class WalletBackupModel @Inject constructor( ) private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt index f7e8fff31f..2e9c2580cd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -160,6 +161,7 @@ internal class WalletHardwareBackupModel @Inject constructor( } private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index bf40c9be77..c0a06bc9e6 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -3,6 +3,11 @@ package com.tangem.features.welcome.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.userwallet.handle import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SignIn +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -36,6 +41,8 @@ internal class WelcomeModel @Inject constructor( private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val walletsRepository: WalletsRepository, + private val trackingContextProxy: TrackingContextProxy, + private val analyticsEventHandler: AnalyticsEventHandler, userWalletsFetcherFactory: UserWalletsFetcher.Factory, ) : Model() { @@ -51,6 +58,14 @@ internal class WelcomeModel @Inject constructor( modelScope.launch { val userWallets = userWalletsListRepository.userWalletsSync() val userWallet = userWallets.first { it.walletId == walletId } + trackingContextProxy.proceedWithContext(userWallet) { + val signInType = when { + !userWallet.isLocked -> SignIn.ButtonWallet.SignInType.NoSecurity + userWallet is UserWallet.Cold -> SignIn.ButtonWallet.SignInType.Card + else -> SignIn.ButtonWallet.SignInType.AccessCode + } + analyticsEventHandler.send(SignIn.ButtonWallet(signInType)) + } onUserWalletClick(userWallet) } }, @@ -64,6 +79,8 @@ internal class WelcomeModel @Inject constructor( userWalletsListRepository.load() wallets.value = walletsFetcher.userWallets.first() + analyticsEventHandler.send(SignIn.ScreenOpened(wallets.value.size)) + launch { walletsFetcher.userWallets .collectLatest { @@ -119,6 +136,7 @@ internal class WelcomeModel @Inject constructor( showUnlockWithBiometricButton = canUnlockWithBiometrics(), addWalletClick = ::addWalletClick, onUnlockWithBiometricClick = { + analyticsEventHandler.send(SignIn.ButtonUnlockAllWithBiometric()) modelScope.launch { userWalletsListRepository.unlockAllWallets() .onRight { @@ -140,6 +158,7 @@ internal class WelcomeModel @Inject constructor( } private fun addWalletClick() { + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) router.push(AppRoute.CreateWalletSelection) } @@ -154,6 +173,7 @@ internal class WelcomeModel @Inject constructor( if (userWallet.isLocked.not()) { // If the wallet is not locked, we can proceed to the wallet screen directly userWalletsListRepository.select(userWallet.walletId) + trackSignInEvent(userWallet, Basic.SignedIn.SignInType.NoSecurity) router.replaceAll(AppRoute.Wallet) return@launch } @@ -203,4 +223,16 @@ internal class WelcomeModel @Inject constructor( } } } + + private suspend fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) { + val walletsCount = userWalletsListRepository.userWalletsSync().size + trackingContextProxy.proceedWithContext(userWallet) { + analyticsEventHandler.send( + event = Basic.SignedIn( + signInType = type, + walletsCount = walletsCount, + ), + ) + } + } } \ No newline at end of file From 70944c8b00ce1ff963d7132da02ebe356811fa85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 11:51:17 +0300 Subject: [PATCH 28/30] Updated on 2026-08-14 --- .../entry/AddExistingWalletStepperStateManager.kt | 6 ------ .../stepper/api/HotWalletStepperComponent.kt | 2 -- .../impl/DefaultHotWalletStepperComponent.kt | 1 - .../stepper/impl/HotWalletStepperModel.kt | 5 ----- .../hotwallet/stepper/impl/ui/HotWalletStepper.kt | 15 ++++----------- .../entry/WalletActivationStepperStateManager.kt | 8 -------- 6 files changed, 4 insertions(+), 33 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt index 587c85328d..0c2edb6dae 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt @@ -15,7 +15,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.wallet_import_seed_navtitle), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( @@ -24,7 +23,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.wallet_import_title), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( @@ -33,7 +31,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.access_code_navtitle), showBackButton = false, showSkipButton = true, - showFeedbackButton = false, ) is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( @@ -42,7 +39,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.access_code_navtitle), showBackButton = true, showSkipButton = true, - showFeedbackButton = false, ) is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( @@ -51,7 +47,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.onboarding_title_notifications), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( @@ -60,7 +55,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.common_done), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt index 169d037699..4f3025382e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt @@ -14,7 +14,6 @@ interface HotWalletStepperComponent : ComposableContentComponent { val title: TextReference, val showBackButton: Boolean, val showSkipButton: Boolean, - val showFeedbackButton: Boolean, ) { companion object { fun initialState() = StepperUM( @@ -23,7 +22,6 @@ interface HotWalletStepperComponent : ComposableContentComponent { title = TextReference.EMPTY, showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt index 320920105b..9b79212aab 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt @@ -34,7 +34,6 @@ internal class DefaultHotWalletStepperComponent @AssistedInject constructor( modifier = modifier, onBackClick = model::onBackClick, onSkipClick = model::onSkipClick, - onFeedbackClick = model::onFeedbackClick, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt index 513718e705..5e26e5570d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt @@ -34,9 +34,4 @@ internal class HotWalletStepperModel @Inject constructor( // TODO send analytics params.callback.onSkipClick() } - - fun onFeedbackClick() { - // TODO send analytics - // openFeedback() - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt index ea029d37be..b6cd497ea4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt @@ -31,7 +31,6 @@ internal fun HotWalletStepper( state: HotWalletStepperComponent.StepperUM, onBackClick: () -> Unit, onSkipClick: () -> Unit, - onFeedbackClick: () -> Unit, modifier: Modifier = Modifier, ) { val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) @@ -47,20 +46,16 @@ internal fun HotWalletStepper( } else { null }, - endButton = when { - state.showSkipButton -> TopAppBarButtonUM.Text( + endButton = if (state.showSkipButton) { + TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) - state.showFeedbackButton -> TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_chat_24, - onClicked = onFeedbackClick, - ) - else -> null + } else { + null }, title = state.title, containerColor = TangemTheme.colors.background.primary, - modifier = modifier, titleAlignment = Alignment.CenterHorizontally, ) @@ -95,11 +90,9 @@ private fun HotWalletStepper_Preview() { title = resourceReference(R.string.common_done), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ), onBackClick = {}, onSkipClick = {}, - onFeedbackClick = {}, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt index 7c3056c55c..22ae7f9af8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt @@ -16,7 +16,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_PHRASE, @@ -24,7 +23,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_CHECK, @@ -32,7 +30,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_COMPLETED, @@ -40,7 +37,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( currentStep = STEP_ACCESS_CODE, @@ -48,7 +44,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.access_code_navtitle), showBackButton = false, showSkipButton = true, - showFeedbackButton = false, ) is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( currentStep = STEP_ACCESS_CODE, @@ -56,7 +51,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.access_code_navtitle), showBackButton = true, showSkipButton = true, - showFeedbackButton = false, ) is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( currentStep = STEP_NOTIFICATIONS, @@ -64,7 +58,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.onboarding_title_notifications), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( currentStep = STEP_DONE, @@ -72,7 +65,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_done), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } From eb96c3be409122437a2e5d3451d39c9187d10b2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 12:53:59 +0300 Subject: [PATCH 29/30] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 19 +++++++++++++++++-- core/res/src/main/res/values-ru/strings.xml | 8 ++++---- core/res/src/main/res/values/strings.xml | 19 +++++++++++++++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b15a7f50b0..15b53db6d3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -3,6 +3,10 @@ アクセスコードがないとウォレットは安全ではありません。 とにかくスキップ アクセスコードが設定されていません + コードを変更 + アクセスコードは、ウォレットのロック解除・資産へのアクセス保護に使用されます + このまま使用 + このアクセスコードは簡単に推測される可能性があります アクセスコードを入力 アクセスコードが間違っています。あと%s回間違えると、モバイルウォレットが削除されます。 アクセスコードが間違っています。あと%s回失敗すると、アプリはロックされます。 @@ -773,6 +777,10 @@ 取引量 これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 + 資産を即時アクセス可能な状態に保ったまま、パワーアップさせよう。%s + 利息モードを有効にする + モバイルウォレットを作成するには、%1$sにアップデートする必要があります + モバイルウォレットを使用するには、%1$s以降が必要です すべてのニュース 最新情報を入手 お使いのデバイスではNFCが使用できません @@ -1897,6 +1905,7 @@ Tangemは、生成された利息に対して15%サービス手数料も徴収します。 ネットワーク手数料が下がるか、残高が最低必要額に達すると、資金は自動的にAaveに供給されます。 過去のリターン + 保有資産に年利%1$s%%を適用 利息モードでのトークンの承認が取り消されました。トークンを開いて再度許可してください。 トークンの承認が必要です ネットワーク接続を確認してください @@ -1911,7 +1920,9 @@ 分散型・自己管理型 このサービスを利用することにより、プロバイダー\n%1$sおよび%2$sに同意するものとします。 Aaveに接続 + 残高に %1$s%% の年利(APY)を適用 Aave %1$s%% • 変動金利 + 変動金利 Aave 平均%s 昨年のリターン @@ -1938,12 +1949,16 @@ 利息モード 利息モードの有効化 利息モード - 利息モードが有効になりました + 利息モードコントラクトのデプロイ + 利息モードを有効にする %1$sがAaveに供給されました - 利息モードが無効になりました + 利息モードを無効にする %1$sがAaveから引き出されました + 利息モードをセットアップ + 利息モードの再有効化 Aaveへの供給 %1$sがAaveに供給されました + Aaveから引き出す 自動 取引のネットワーク手数料をカバーするために、 %1$s %2$sを追加してください。 %s手数料を支払えません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index db0fefa23f..9cf3d2fac4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -274,7 +274,7 @@ Из Из %s Синхронизировать адреса - Начать зарабатывать + Начать Получить токен К провайдеру Перейти в токен @@ -1075,7 +1075,7 @@ Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта или кольцо Сканировать Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку. - Приготовьте свой Tangem! + Приготовьте устройство Tangem! Уведомление безопасности Нет Да @@ -1854,9 +1854,9 @@ Режим доходности Включение режима доходности Режим доходности - Режим доходности включен + Включение режима доходности %1$s отправлено в Aave - Режим доходности выключен + Отключение режима доходности %1$s выведено из Aave Перевод средств в Aave %1$s отправлено в Aave diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4c14322aea..497893d2c4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -3,6 +3,10 @@ Without an access code, your wallet is not secure. Skip anyway Access code not set + Change code + Your access code will be used to unlock your wallet and to protect access to the assets + Use anyway + This access code can be easily guessed Enter access code Wrong access code. Your mobile wallet will be deleted after %s more incorrect attempts. Wrong access code. The app will be locked after %s more failed attempts @@ -788,6 +792,10 @@ Volume Pull this up or tap the search bar to add tokens directly from the market Add tokens + Power up your assets while supplying them with instant access. %s + Activate Yield Mode + You must update to %1$s in order to create mobile wallet + Mobile Wallet requires %1$s or later All news Stay in the loop NFC is not available on your device @@ -1973,6 +1981,7 @@ Tangem also takes a 15% service fee on yield generated. Your funds will be automatically supplied to Aave once network fees are lower or your balance meets the minimum required amount. Historical returns + "Enable %1$s%% APY on your balance" Approval for your token in Yield Mode has been revoked. Open the token to grant permission again. Token approval needed Check your network connection @@ -1987,7 +1996,9 @@ Decentralized and self-custodial By using this service, you agree with provider\n%1$s and %2$s Connect to Aave + Enable %1$s%% APY\non your balance Aave %1$s%% • Variable Interest Rate + Variable Interest Rate Aave Avg %s Last year\'s returns @@ -2014,12 +2025,16 @@ Yield Mode Enabling Yield Mode Yield Mode - Yield Mode enabled + Yield Mode contract deploy + Yield Mode enable %1$s supplied to Aave - Yield Mode disabled + Yield Mode disable %1$s withdrawn from Aave + Yield Mode initialize + Yield Mode reactivate Supply to Aave %1$s supplied to Aave + Withdraw from Aave Automatic Add some %1$s %2$s to cover the network fee for transactions. Unable to cover %s fee From def92569d36a7dd078ec50982a99bff5933da13c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 10:35:43 +0400 Subject: [PATCH 30/30] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 18 ++--- data/nft/build.gradle.kts | 8 ++ .../tangem/data/nft/DefaultNFTRepository.kt | 30 +++++-- .../com/tangem/data/nft/di/NFTDataModule.kt | 40 +++------- .../com/tangem/data/nft/NFTCleanerTest.kt | 78 +++++++++++++++++++ domain/account/status/build.gradle.kts | 1 + domain/nft/build.gradle.kts | 2 +- .../domain/nft/DisableWalletNFTUseCase.kt | 9 ++- .../domain/nft/GetNFTCollectionsUseCase.kt | 19 +++-- .../domain/nft/GetNFTNetworksUseCase.kt | 24 ++++-- .../ObserveAndClearNFTCacheIfNeedUseCase.kt | 6 +- .../domain/nft/repository/NFTRepository.kt | 4 +- .../com/tangem/domain/nft/utils/NFTCleaner.kt | 30 +++++++ 13 files changed, 196 insertions(+), 73 deletions(-) create mode 100644 data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 2ed2a31346..2fbc11170b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -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, diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 64d14695e9..4124798b29 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -12,6 +12,10 @@ android { namespace = "com.tangem.data.nft" } +tasks.withType().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) } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 7ceb082dd5..af884a8963 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.nft +import android.content.Context import android.content.res.Resources import arrow.core.Either import com.tangem.blockchain.common.Blockchain @@ -26,15 +27,19 @@ import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -49,9 +54,10 @@ internal class DefaultNFTRepository @Inject constructor( private val userWalletsStore: UserWalletsStore, private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, - resources: Resources, -) : NFTRepository { + @ApplicationContext private val context: Context, +) : NFTRepository, NFTCleaner { + private val resources: Resources by lazy { context.resources } private val networkJobs = ConcurrentHashMap() private val collectionJobs = ConcurrentHashMap() private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) @@ -218,10 +224,22 @@ internal class DefaultNFTRepository @Inject constructor( assetIdentifier = assetIdConverter.convertBack(assetIdentifier), ) - override suspend fun clearCache(userWalletId: UserWalletId, networks: List) { - networks.forEach { - getNFTPersistenceStore(userWalletId, it).clear() - getNFTRuntimeStore(userWalletId, it).clear() + // NFTCleaner implementation + override suspend fun invoke(userWalletId: UserWalletId, networks: Set) { + if (networks.isEmpty()) { + Timber.d("No networks to clear for wallet: $userWalletId") + return + } + + networks.forEach { network -> + runSuspendCatching { + getNFTPersistenceStore(userWalletId = userWalletId, network = network).clear() + // FIXME: nftRuntimeStore is created with only network, so clearing it may affect other wallets + // nftRuntimeStoreFactory.provide(network = network).clear() + } + .onFailure { throwable -> + Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId") + } } } diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt index beeca8c0c2..d5fd54fa62 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt @@ -1,45 +1,23 @@ package com.tangem.data.nft.di -import android.content.Context -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.network.NetworkFactory import com.tangem.data.nft.DefaultNFTRepository -import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory -import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.domain.nft.utils.NFTCleaner +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object NFTDataModule { +internal interface NFTDataModule { - @Provides + @Binds @Singleton - fun provideNFTRepository( - @ApplicationContext context: Context, - nftPersistenceStoreFactory: NFTPersistenceStoreFactory, - nftRuntimeStoreFactory: NFTRuntimeStoreFactory, - walletManagersFacade: WalletManagersFacade, - dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, - userWalletsStore: UserWalletsStore, - networkFactory: NetworkFactory, - ): NFTRepository = DefaultNFTRepository( - nftPersistenceStoreFactory = nftPersistenceStoreFactory, - nftRuntimeStoreFactory = nftRuntimeStoreFactory, - walletManagersFacade = walletManagersFacade, - dispatchers = dispatchers, - excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, - networkFactory = networkFactory, - resources = context.resources, - ) + fun bindNFTRepository(defaultNFTRepository: DefaultNFTRepository): NFTRepository + + @Binds + @Singleton + fun bindNFTCleaner(defaultNFTRepository: DefaultNFTRepository): NFTCleaner } \ No newline at end of file diff --git a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt new file mode 100644 index 0000000000..1a27526992 --- /dev/null +++ b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt @@ -0,0 +1,78 @@ +package com.tangem.data.nft + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.nft.NFTPersistenceStore +import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory +import com.tangem.datasource.local.nft.NFTRuntimeStore +import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class NFTCleanerTest { + + private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk() + private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk() + + private val nftCleaner = DefaultNFTRepository( + nftPersistenceStoreFactory = nftPersistenceStoreFactory, + nftRuntimeStoreFactory = nftRuntimeStoreFactory, + walletManagersFacade = mockk(), + dispatchers = mockk(), + userWalletsStore = mockk(), + networkFactory = mockk(), + excludedBlockchains = mockk(), + context = mockk(), + ) + + private val userWalletId = UserWalletId("011") + + @AfterEach + fun tearDown() { + clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory) + } + + @Test + fun `should call invoke with multiple networks`() = runTest { + // Arrange + val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory() + val networks = mockCryptoCurrencyFactory.ethereumAndStellar.map(CryptoCurrency.Coin::network) + val persistenceByNetwork = networks.associateWith { mockk(relaxUnitFun = true) } + val runtimeByNetwork = networks.associateWith { mockk(relaxUnitFun = true) } + + networks.forEach { network -> + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceByNetwork[network]!! + every { nftRuntimeStoreFactory.provide(network) } returns runtimeByNetwork[network]!! + } + + // Act + nftCleaner.invoke(userWalletId = userWalletId, networks = networks.toSet()) + + // Assert + coVerifyOrder { + networks.forEach { network -> + nftPersistenceStoreFactory.provide(userWalletId, network) + persistenceByNetwork[network]!!.clear() + // nftRuntimeStoreFactory.provide(network) + // runtimeByNetwork[network]!!.clear() + } + } + } + + @Test + fun `should handle empty networks set`() = runTest { + // Act + nftCleaner.invoke(userWalletId, emptySet()) + + // Assert + coVerify(inverse = true) { + nftPersistenceStoreFactory.provide(userWalletId = any(), network = any()) + nftRuntimeStoreFactory.provide(network = any()) + } + } +} \ No newline at end of file diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index e896b83ddf..5d47d084f1 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { api(projects.domain.quotes) api(projects.domain.models) api(projects.domain.networks) + api(projects.domain.nft) api(projects.domain.referral) api(projects.domain.staking) api(projects.domain.tokens) diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index e7b3973752..4e39ec51eb 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { // region Project – Domain implementation(projects.domain.core) - implementation(projects.domain.account.status) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.networks) implementation(projects.domain.nft.models) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt index c671f030a9..04e223a470 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt @@ -1,15 +1,16 @@ package com.tangem.domain.nft +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.wallets.repository.WalletsRepository class DisableWalletNFTUseCase( private val walletsRepository: WalletsRepository, - private val nftRepository: NFTRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val nftCleaner: NFTCleaner, ) { suspend operator fun invoke(userWalletId: UserWalletId) { @@ -20,7 +21,7 @@ class DisableWalletNFTUseCase( ) .orEmpty() - val networks = currencies.map { it.network } - nftRepository.clearCache(userWalletId, networks) + val networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network) + nftCleaner(userWalletId, networks) } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index e7894f68c1..db4c13cacc 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -1,9 +1,8 @@ package com.tangem.domain.nft 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.models.account.Account -import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTCollections @@ -16,7 +15,7 @@ import kotlinx.coroutines.flow.* class GetNFTCollectionsUseCase( private val currenciesRepository: CurrenciesRepository, private val nftRepository: NFTRepository, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val singleAccountListSupplier: SingleAccountListSupplier, private val accountsFeatureToggles: AccountsFeatureToggles, ) { @@ -33,13 +32,17 @@ class GetNFTCollectionsUseCase( } } + @OptIn(ExperimentalCoroutinesApi::class) fun invokeForAccounts(userWalletId: UserWalletId): Flow { - fun AccountStatus.flowOfNFTCollections(): Flow>> = - nftCollections(userWalletId, this.flattenCurrencies().map { it.currency }) - .map { nfts -> this.account to nfts } + fun Account.flowOfNFTCollections(): Flow>> { + val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() - return singleAccountStatusListSupplier(userWalletId) - .mapLatest { statusList -> statusList.accountStatuses.map { it.flowOfNFTCollections() } } + return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList()) + .map { nfts -> this to nfts } + } + + return singleAccountListSupplier(userWalletId) + .mapLatest { statusList -> statusList.accounts.map { it.flowOfNFTCollections() } } .flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } } } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt index e3f8d2c71e..3332f38965 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt @@ -1,12 +1,14 @@ package com.tangem.domain.nft -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTNetworks import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -14,19 +16,25 @@ import kotlinx.coroutines.flow.mapNotNull class GetNFTNetworksUseCase( private val currenciesRepository: CurrenciesRepository, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val singleAccountListSupplier: SingleAccountListSupplier, private val nftRepository: NFTRepository, ) { + @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(portfolioId: PortfolioId): Flow = when (portfolioId) { - is PortfolioId.Account -> singleAccountStatusListSupplier(portfolioId.userWalletId) - .map { it.accountStatuses } - .mapNotNull { accountStatuses -> accountStatuses.find { it.account.accountId == portfolioId.accountId } } - .map { accountStatus -> accountStatus.flattenCurrencies().map { it.currency } } - .mapLatest { it.toNFTNetworks(portfolioId.userWalletId) } - is PortfolioId.Wallet -> + is PortfolioId.Account -> { + singleAccountListSupplier(portfolioId.userWalletId) + .mapNotNull { accountList -> + val account = accountList.accounts.find { it.accountId == portfolioId.accountId } + + (account as? Account.CryptoPortfolio)?.cryptoCurrencies?.toList() + } + .mapLatest { it.toNFTNetworks(portfolioId.userWalletId) } + } + is PortfolioId.Wallet -> { currenciesRepository .getWalletCurrenciesUpdates(portfolioId.userWalletId) .map { cryptoCurrencies -> cryptoCurrencies.toNFTNetworks(portfolioId.userWalletId) } + } } private suspend fun List.toNFTNetworks(userWalletId: UserWalletId): NFTNetworks { diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt index a1aaa94941..86e2e2ac34 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt @@ -6,12 +6,12 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.flow.* class ObserveAndClearNFTCacheIfNeedUseCase( - private val nftRepository: NFTRepository, + private val nftCleaner: NFTCleaner, private val currenciesRepository: CurrenciesRepository, private val accountsFeatureToggles: AccountsFeatureToggles, private val singleAccountListSupplier: SingleAccountListSupplier, @@ -26,7 +26,7 @@ class ObserveAndClearNFTCacheIfNeedUseCase( .distinctUntilChanged() .onEach { removedNetworks -> if (removedNetworks.isNotEmpty()) { - nftRepository.clearCache(userWalletId, removedNetworks.toList()) + nftCleaner(userWalletId, removedNetworks) } } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 52a74367a2..eea9a7acd3 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -2,11 +2,11 @@ package com.tangem.domain.nft.repository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow interface NFTRepository { @@ -32,6 +32,4 @@ interface NFTRepository { suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? - - suspend fun clearCache(userWalletId: UserWalletId, networks: List) } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt new file mode 100644 index 0000000000..f7b993b10c --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.nft.utils + +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Cleans up NFT data for a given user wallet and network(s). + * +[REDACTED_AUTHOR] + */ +interface NFTCleaner { + + /** + * Cleans up NFT data for a given user wallet and single network. + * + * @param userWalletId the user wallet id + * @param network the network to clean up + */ + suspend operator fun invoke(userWalletId: UserWalletId, network: Network) { + invoke(userWalletId = userWalletId, networks = setOf(network)) + } + + /** + * Cleans up NFT data for a given user wallet and multiple networks. + * + * @param userWalletId the user wallet id + * @param networks the set of networks to clean up + */ + suspend operator fun invoke(userWalletId: UserWalletId, networks: Set) +} \ No newline at end of file