From e3ea28584bf9c7275dfb8c29333d0edf5c6e4124 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Nov 2025 18:11:10 +0200 Subject: [PATCH 001/130] 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 002/130] 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 003/130] 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 004/130] 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 005/130] 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 006/130] 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 007/130] 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 008/130] 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 009/130] 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 010/130] 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 011/130] 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 012/130] 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 013/130] 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 014/130] 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 015/130] 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 016/130] 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 017/130] 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 018/130] 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 019/130] 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 020/130] 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 021/130] 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 022/130] 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 023/130] 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 024/130] 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 025/130] 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 026/130] 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 027/130] 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 028/130] 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 029/130] 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 030/130] 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 From d99d5ba6744a174419554b0d00aea759b76e08f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 11:25:09 +0000 Subject: [PATCH 031/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 07be659f64..487e084342 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31-1314" +tangemBlockchainSdk = "develop-1309" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +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 9b99ee07f8d6ffa4c8afa4e0f29cdeba00a52126 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 14:26:16 +0300 Subject: [PATCH 032/130] Updated on 2026-08-14 --- .../features/onramp/selecttoken/model/OnrampOperationModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index a8957a1d0c..cad8e72a0a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -49,11 +49,11 @@ internal class OnrampOperationModel @Inject constructor( private val rampStateManager: RampStateManager, ) : Model() { + private val params: Params = paramsContainer.require() + val state: StateFlow field = MutableStateFlow(value = getInitialState()) - private val params: Params = paramsContainer.require() - private val selectedUserWallet = getWalletsUseCase.invokeSync() .first { it.walletId == params.userWalletId } From a08e090a8fcfe39887e0c3c49c901b3e7cbbf076 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 18:49:34 +0400 Subject: [PATCH 033/130] Updated on 2026-08-14 --- .../account/GetWalletAccountsResponse.kt | 1 + .../account/converter/AccountListConverter.kt | 1 + .../GetWalletAccountsResponseConverter.kt | 1 + .../DefaultWalletAccountsResponseFactory.kt | 1 + .../account/converter/AccountConverterExt.kt | 2 + .../converter/AccountListConverterTest.kt | 1 + .../FetchWalletAccountsErrorHandlerTest.kt | 1 + .../DefaultMainAccountTokensMigrationTest.kt | 1 + ...efaultWalletAccountsResponseFactoryTest.kt | 4 + .../utils/GetWalletAccountsResponseExtTest.kt | 8 + .../domain/account/models/AccountList.kt | 7 + .../account/models/AccountStatusList.kt | 2 + .../usecase/ApplyAccountListSortingUseCase.kt | 3 +- .../domain/account/models/AccountListTest.kt | 16 ++ .../ApplyAccountListSortingUseCaseTest.kt | 1 + .../DefaultSingleAccountStatusListProducer.kt | 2 + .../usecase/ApplyTokenListSortingUseCaseV2.kt | 1 + ...aultSingleAccountStatusListProducerTest.kt | 5 + .../ApplyTokenListSortingUseCaseTest.kt | 1 + .../ArchiveCryptoPortfolioUseCaseTest.kt | 1 + .../ToggleTokenListGroupingUseCaseV2Test.kt | 2 + .../ToggleTokenListSortingUseCaseV2Test.kt | 2 + .../accounts/entity/AccountsUM.kt | 35 ++- .../accounts/ui/AccountsScreen.kt | 2 +- .../viewmodel/TesterAccountsViewModel.kt | 200 +++++++++++------- .../java/com/tangem/test/mock/MockAccounts.kt | 2 + 26 files changed, 216 insertions(+), 87 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt index 757436e0fc..6b484534d2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -19,6 +19,7 @@ data class GetWalletAccountsResponse( @Json(name = "group") val group: GroupType?, @Json(name = "sort") val sort: SortType?, @Json(name = "totalAccounts") val totalAccounts: Int, + @Json(name = "totalArchivedAccounts") val totalArchivedAccounts: Int, ) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt index 2da5ac22bc..dc8ddf5dfa 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -36,6 +36,7 @@ internal class AccountListConverter @AssistedInject constructor( userWalletId = userWallet.walletId, accounts = value.accounts.map(cryptoPortfolioConverter::convert), totalAccounts = value.wallet.totalAccounts, + totalArchivedAccounts = value.wallet.totalArchivedAccounts, sortType = sortType, groupType = groupType, ) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt index 3f741ba0b5..72e746c1bc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt @@ -27,6 +27,7 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor( group = TokensGroupTypeConverter.convertBack(value.groupType), sort = TokensSortTypeConverter.convertBack(value.sortType), totalAccounts = value.totalAccounts, + totalArchivedAccounts = value.totalArchivedAccounts, ), accounts = value.accounts .filterIsInstance() diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt index 87a090099e..023e9161ec 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -43,6 +43,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor( group = response.group, sort = response.sort, totalAccounts = accountDTOs.size, + totalArchivedAccounts = 0, ), accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt index a62a9840fb..f019c1b5fe 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -53,6 +53,7 @@ internal fun createGetWalletAccountsResponse( group = groupType, sort = sortType, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = buildList { createWalletAccountDTO( @@ -79,6 +80,7 @@ internal fun createAccountList( userWalletId = userWalletId, accounts = listOf(createCryptoPortfolio(userWalletId)), totalAccounts = 1, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt index e192321af6..d97ef69569 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -137,6 +137,7 @@ class AccountListConverterTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 3c8951fa2b..88e44f4db4 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -182,6 +182,7 @@ class FetchWalletAccountsErrorHandlerTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountDTO), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt index fb8f294326..025b7d07bd 100644 --- a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt @@ -179,6 +179,7 @@ class DefaultMainAccountTokensMigrationTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(mainAccount, selectedAccount), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt index 674c15d34b..86949ca186 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -81,6 +81,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -135,6 +136,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = defaultResponse.group, sort = defaultResponse.sort, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountsDTO.copy(tokens = listOf(token))), unassignedTokens = emptyList(), @@ -190,6 +192,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = defaultResponse.group, sort = defaultResponse.sort, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -228,6 +231,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = userTokensResponse.group, sort = userTokensResponse.sort, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountsDTO.copy(tokens = userTokensResponse.tokens)), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt index 9124a77517..4cea27b28c 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt @@ -32,6 +32,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -55,6 +56,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = emptyList(), @@ -84,6 +86,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account1, account2, account3), unassignedTokens = emptyList(), @@ -110,6 +113,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -141,6 +145,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = listOf(token2), @@ -178,6 +183,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account1, account2), unassignedTokens = listOf(token1, token2), @@ -217,6 +223,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = emptyList(), @@ -248,6 +255,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = listOf(token1, token2), diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index b9378ac209..db2dc28e25 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -28,6 +28,7 @@ data class AccountList private constructor( val userWalletId: UserWalletId, val accounts: List, val totalAccounts: Int, + val totalArchivedAccounts: Int, val sortType: TokensSortType, val groupType: TokensGroupType, ) { @@ -60,6 +61,7 @@ data class AccountList private constructor( userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + totalArchivedAccounts = this.totalArchivedAccounts, sortType = this.sortType, groupType = this.groupType, ) @@ -82,6 +84,7 @@ data class AccountList private constructor( userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + totalArchivedAccounts = this.totalArchivedAccounts, sortType = this.sortType, groupType = this.groupType, ) @@ -152,6 +155,7 @@ data class AccountList private constructor( companion object { const val MAX_ACCOUNTS_COUNT = 20 + const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000 private const val MAX_MAIN_ACCOUNTS_COUNT = 1 /** @@ -166,6 +170,7 @@ data class AccountList private constructor( userWalletId: UserWalletId, accounts: List, totalAccounts: Int, + totalArchivedAccounts: Int, sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): Either = either { @@ -200,6 +205,7 @@ data class AccountList private constructor( userWalletId = userWalletId, accounts = accounts, totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, sortType = sortType, groupType = groupType, ) @@ -225,6 +231,7 @@ data class AccountList private constructor( ), ), totalAccounts = 1, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index f7c9a12dc5..8309ed3403 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -26,6 +26,7 @@ data class AccountStatusList( val userWalletId: UserWalletId, val accountStatuses: List, val totalAccounts: Int, + val totalArchivedAccounts: Int, val totalFiatBalance: TotalFiatBalance, val sortType: TokensSortType, val groupType: TokensGroupType, @@ -47,6 +48,7 @@ data class AccountStatusList( userWalletId = userWalletId, accounts = accountStatuses.map(AccountStatus::account), totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, sortType = sortType, groupType = groupType, ) diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt index bb0714b5b1..9c7d51c56f 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt @@ -55,10 +55,11 @@ class ApplyAccountListSortingUseCase( val updatedAccountList = withError( transform = { Error.DataOperationFailed("Unable to create AccountList: $it") }, ) { - AccountList( + AccountList.invoke( userWalletId = accountList.userWalletId, accounts = sortedAccounts, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 34f6603024..a242546b21 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -87,6 +87,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = accounts, totalAccounts = accounts.size, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) @@ -96,6 +97,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = accounts, totalAccounts = accounts.size, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) @@ -110,6 +112,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = model.accounts, totalAccounts = model.totalAccounts, + totalArchivedAccounts = 0, ) // Assert @@ -206,12 +209,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(mainAccount, newAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ), ) }, @@ -226,12 +231,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(newAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ), ) }, @@ -252,6 +259,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.MainAccountNotFound.left(), @@ -268,6 +276,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), @@ -284,6 +293,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.DuplicateAccountNames.left(), @@ -296,6 +306,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = createAccounts(count = 20), totalAccounts = 20, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = createAccount(derivationIndex = 21), expected = AccountList.Error.ExceedsMaxAccountsCount.left(), @@ -335,12 +346,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount, secondaryAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = secondaryAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ), ) }, @@ -354,6 +367,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ) MinusTestModel( @@ -372,6 +386,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = mainAccount, expected = AccountList.Error.EmptyAccountsList.left(), @@ -388,6 +403,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount, secondaryAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = mainAccount, expected = AccountList.Error.MainAccountNotFound.left(), diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt index 2d19983849..b709863d71 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt @@ -157,6 +157,7 @@ class ApplyAccountListSortingUseCaseTest { userWalletId = accountList.userWalletId, accounts = accountList.accounts.reversed(), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ).getOrNull()!! diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 8b3efcfba9..bf10f519ad 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -68,6 +68,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo accountStatuses = accountStatuses.toList(), totalAccounts = accountList.totalAccounts, totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) @@ -194,6 +195,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo } }, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = accountList.sortType, groupType = accountList.groupType, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt index a3a2854c45..f3a5fdeec0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt @@ -106,6 +106,7 @@ class ApplyTokenListSortingUseCaseV2( userWalletId = accountList.userWalletId, accounts = accountList.accounts.sortTokens(sortedTokensIdsByAccount, errors), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE, groupType = if (isGroupedByNetwork) TokensGroupType.NETWORK else TokensGroupType.NONE, ).getOrElse { diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt index 15c437d7aa..0b19a4cd9f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt @@ -90,6 +90,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -132,6 +133,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -153,6 +155,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = updatedAccountList.sortType, groupType = updatedAccountList.groupType, @@ -192,6 +195,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -273,6 +277,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = accountList.sortType, groupType = accountList.groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index 48026d148b..c838e40466 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -348,6 +348,7 @@ internal class ApplyTokenListSortingUseCaseTest { customAccount, // unchanged due to error ), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index 109fe9d65d..d3257d26ed 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -295,6 +295,7 @@ class ArchiveCryptoPortfolioUseCaseTest { ) }, totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = sortType, groupType = groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt index 016e88d152..9dadb85e4f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt @@ -42,6 +42,7 @@ class ToggleTokenListGroupingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = emptyList(), totalAccounts = 0, + totalArchivedAccounts = 0, totalFiatBalance = TotalFiatBalance.Failed, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, @@ -205,6 +206,7 @@ class ToggleTokenListGroupingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = listOf(accountStatus), totalAccounts = 1, + totalArchivedAccounts = 0, totalFiatBalance = tokenList.totalFiatBalance, sortType = tokenList.sortedBy, groupType = groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt index d5806c8bb1..344dc8641e 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt @@ -42,6 +42,7 @@ class ToggleTokenListSortingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = emptyList(), totalAccounts = 0, + totalArchivedAccounts = 0, totalFiatBalance = TotalFiatBalance.Failed, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, @@ -145,6 +146,7 @@ class ToggleTokenListSortingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = listOf(accountStatus), totalAccounts = 1, + totalArchivedAccounts = 0, totalFiatBalance = tokenList.totalFiatBalance, sortType = tokenList.sortedBy, groupType = TokensGroupType.NONE, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt index 245c9a99f6..d196e87bfd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt @@ -13,11 +13,40 @@ internal data class AccountsUM( ) { data class Button( - val title: String, + val id: ID, + val title: String = createTitle(id), val isInProgress: Boolean = false, val isEnabled: Boolean = true, - val onClick: (title: String) -> Unit, - ) + val onClick: () -> Unit, + ) { + + enum class ID { + ShowAccountList, + FetchAccounts, + FillOutList, + FillOutArchivedList, + ArchiveAll, + SortByDerivationIndex, + ClearETag, + } + + fun reset(): Button { + return copy(title = createTitle(id), isInProgress = false, isEnabled = true) + } + + companion object { + + private fun createTitle(id: ID): String = when (id) { + ID.ShowAccountList -> "Show the account list" + ID.FetchAccounts -> "Fetch accounts" + ID.FillOutList -> "Fill out the list (up to 20)" + ID.FillOutArchivedList -> "Fill out the archived list" + ID.ArchiveAll -> "Archive all" + ID.SortByDerivationIndex -> "Sort by derivation index" + ID.ClearETag -> "Clear ETag" + } + } + } data class WalletSelector( val selected: UserWallet?, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt index 1f19494869..46f9cc8aba 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt @@ -206,7 +206,7 @@ private fun LazyListScope.ManageAccountsButtons(buttons: ImmutableList PrimaryButton( text = button.title, - onClick = { button.onClick(button.title) }, + onClick = button.onClick, modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth(), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 41c3f707e8..0d855a3880 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -60,7 +60,7 @@ internal class TesterAccountsViewModel @Inject constructor( userWallets.firstOrNull()?.let(::onWalletSelect) - userWallets.mapIndexed { index, userWallet -> + userWallets.map { userWallet -> singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId = userWallet.walletId)) .distinctUntilChanged() .onEach { accountList -> @@ -102,12 +102,16 @@ internal class TesterAccountsViewModel @Inject constructor( }, ), buttons = persistentListOf( - AccountsUM.Button(title = "Show the account list") { updateAccountsList() }, - AccountsUM.Button(title = "Fetch accounts", onClick = ::fetchAccounts), - AccountsUM.Button(title = "Fill out the list (up to 20)", onClick = ::fillOutAccountList), - AccountsUM.Button(title = "Archive all", onClick = ::archiveAllAccounts), - AccountsUM.Button(title = "Sort by derivation index", onClick = ::sortAccountsByIndex), - AccountsUM.Button(title = "Clear ETag") { clearETag() }, + AccountsUM.Button(id = AccountsUM.Button.ID.ShowAccountList, onClick = ::updateAccountsList), + AccountsUM.Button(id = AccountsUM.Button.ID.FetchAccounts, onClick = ::fetchAccounts), + AccountsUM.Button(id = AccountsUM.Button.ID.FillOutList, onClick = ::onFillOutClick), + AccountsUM.Button( + id = AccountsUM.Button.ID.FillOutArchivedList, + onClick = ::fillOutArchivedAccountList, + ), + AccountsUM.Button(id = AccountsUM.Button.ID.ArchiveAll, onClick = ::onArchiveAllClick), + AccountsUM.Button(id = AccountsUM.Button.ID.SortByDerivationIndex, onClick = ::sortAccountsByIndex), + AccountsUM.Button(id = AccountsUM.Button.ID.ClearETag) { clearETag() }, ), ) } @@ -137,19 +141,15 @@ internal class TesterAccountsViewModel @Inject constructor( } } - private fun fetchAccounts(title: String) { - viewModelScope.launch { - val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return@launch - - toggleButtonProgress(title = title, isInProgress = true) + private fun fetchAccounts() { + val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return + withProgress(id = AccountsUM.Button.ID.FetchAccounts) { withContext(dispatchers.default) { singleAccountListFetcher( params = SingleAccountListFetcher.Params(userWalletId = userWalletId), ) } - - toggleButtonProgress(title = title, isInProgress = false) } } @@ -160,84 +160,113 @@ internal class TesterAccountsViewModel @Inject constructor( } } - private fun fillOutAccountList(title: String) { + private fun onFillOutClick() { val userWalletId = getUserWallet()?.walletId ?: return - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) + withProgress(id = AccountsUM.Button.ID.FillOutList) { + fillOutAccountList(userWalletId = userWalletId) + } + } - withContext(dispatchers.default) { - var accountList = walletAccounts.value[userWalletId] ?: return@withContext - val occupiedIndexes = accountList.accounts - .filterIsInstance() - .map { it.derivationIndex.value } - .toSet() + private fun fillOutArchivedAccountList() { + val userWalletId = getUserWallet()?.walletId ?: return + val accountList = walletAccounts.value[userWalletId] ?: return + if (accountList.totalArchivedAccounts == AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT) return - var nextIndex = 0 - @Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu - while (accountList.canAddMoreAccounts) { - while (occupiedIndexes.contains(nextIndex)) { - nextIndex++ - } + val id = AccountsUM.Button.ID.FillOutArchivedList - val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break + viewModelScope.launch(dispatchers.default) { + var currentTotalArchived = accountList.totalArchivedAccounts - val newAccount = Account.CryptoPortfolio.invoke( - accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex), - name = "Account #$nextIndex", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = nextIndex, - cryptoCurrencies = emptySet(), + while (currentTotalArchived < AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT) { + updateButton(id) { button -> + button.copy( + title = "In progress... ($currentTotalArchived/${AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT})", + isEnabled = false, ) - .getOrNull() - ?: break - - (accountList + newAccount) - .onRight { accountList = it } - .getOrNull() - ?: break - - nextIndex++ } - accountsCRUDRepository.saveAccounts(accountList) + fillOutAccountList(userWalletId = userWalletId) + archiveAll(userWalletId = userWalletId) + + accountsCRUDRepository.getAccountListSync(userWalletId).onSome { + currentTotalArchived = it.totalArchivedAccounts + } } - toggleButtonProgress(title = title, isInProgress = false) + updateButton(id = id, button = AccountsUM.Button::reset) } } - private fun archiveAllAccounts(title: String) { - val userWalletId = getUserWallet()?.walletId ?: return - val accountList = walletAccounts.value[userWalletId] ?: return + private suspend fun fillOutAccountList(userWalletId: UserWalletId) { + withContext(dispatchers.default) { + var accountList = walletAccounts.value[userWalletId] ?: return@withContext - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) + var nextIndex = accountList.totalAccounts + @Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu + while (accountList.canAddMoreAccounts) { + val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break - withContext(dispatchers.default) { - val updatedAccountList = AccountList.invoke( - userWalletId = accountList.userWalletId, - accounts = listOf(accountList.mainAccount), - totalAccounts = accountList.totalAccounts, - sortType = accountList.sortType, - groupType = accountList.groupType, + val newAccount = Account.CryptoPortfolio.invoke( + accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex), + name = "Account #$nextIndex", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = nextIndex, + cryptoCurrencies = emptySet(), ) - .getOrElse { return@withContext } + .getOrNull() + ?: break - accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + (accountList + newAccount) + .onRight { accountList = it } + .getOrNull() + ?: break + + nextIndex++ } - toggleButtonProgress(title = title, isInProgress = false) + accountsCRUDRepository.saveAccounts(accountList) } } - private fun sortAccountsByIndex(title: String) { + private fun onArchiveAllClick() { + val userWalletId = getUserWallet()?.walletId ?: return + + withProgress(id = AccountsUM.Button.ID.ArchiveAll) { + archiveAll(userWalletId = userWalletId) + } + } + + private suspend fun archiveAll(userWalletId: UserWalletId) { + val accountList = walletAccounts.value[userWalletId] ?: return + val possibleToArchive = AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT - accountList.totalArchivedAccounts + + if (possibleToArchive == 0) return + + withContext(dispatchers.default) { + val updatedAccountList = AccountList.invoke( + userWalletId = accountList.userWalletId, + accounts = if (possibleToArchive > AccountList.MAX_ACCOUNTS_COUNT - 1) { + listOf(accountList.mainAccount) + } else { + accountList.accounts.subList(fromIndex = 0, toIndex = accountList.accounts.size - possibleToArchive) + }, + totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) + .getOrElse { return@withContext } + + accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + } + } + + private fun sortAccountsByIndex() { val userWalletId = getUserWallet()?.walletId ?: return val accountList = walletAccounts.value[userWalletId] ?: return - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) - + withProgress(id = AccountsUM.Button.ID.SortByDerivationIndex) { withContext(dispatchers.default) { val sortedAccountList = AccountList.invoke( userWalletId = accountList.userWalletId, @@ -245,6 +274,7 @@ internal class TesterAccountsViewModel @Inject constructor( .filterIsInstance() .sortedBy { it.derivationIndex.value }, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) @@ -252,8 +282,6 @@ internal class TesterAccountsViewModel @Inject constructor( accountsCRUDRepository.saveAccounts(accountList = sortedAccountList) } - - toggleButtonProgress(title = title, isInProgress = false) } } @@ -266,22 +294,30 @@ internal class TesterAccountsViewModel @Inject constructor( ?: persistentListOf() } - private fun toggleButtonProgress(title: String, isInProgress: Boolean) { - uiState.update { state -> - state.copy( - buttons = state.buttons.updateButton(title) { - it.copy(isInProgress = isInProgress) - }, - ) + private fun withProgress(id: AccountsUM.Button.ID, block: suspend () -> Unit) { + viewModelScope.launch { + toggleButtonProgress(id = id, isInProgress = true) + try { + block() + } finally { + toggleButtonProgress(id = id, isInProgress = false) + } } } - private fun ImmutableList.updateButton( - title: String, - button: (AccountsUM.Button) -> AccountsUM.Button, - ): ImmutableList { - return this - .map { if (it.title == title) button(it) else it } - .toImmutableList() + private fun toggleButtonProgress(id: AccountsUM.Button.ID, isInProgress: Boolean) { + updateButton(id) { + it.copy(isInProgress = isInProgress) + } + } + + private fun updateButton(id: AccountsUM.Button.ID, button: (AccountsUM.Button) -> AccountsUM.Button) { + uiState.update { state -> + state.copy( + buttons = state.buttons + .map { if (it.id == id) button(it) else it } + .toImmutableList(), + ) + } } } \ No newline at end of file diff --git a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt index c8b8f57517..f275e2b0d2 100644 --- a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt +++ b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt @@ -19,12 +19,14 @@ object MockAccounts { fun createAccountList( activeAccounts: Int, totalAccounts: Int = activeAccounts, + totalArchivedAccounts: Int = 0, userWalletId: UserWalletId = this.userWalletId, ): AccountList { return AccountList( userWalletId = userWalletId, accounts = createAccounts(count = activeAccounts, userWalletId = userWalletId), totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, ).getOrNull()!! } From daf2a6b636c2b72bdc072166e88ef73e32c7cd10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Dec 2025 11:38:35 +0300 Subject: [PATCH 034/130] Updated on 2026-08-14 --- .../tap/features/details/ui/cardsettings/CardSettingsScreen.kt | 3 --- 1 file changed, 3 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 c5d8bc5cb6..7cffbf079d 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 @@ -116,9 +116,6 @@ private fun ScanCardContent(onScanCardClick: () -> Unit) { 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( From 81a7b760aea616c47e3ab567d685d3c4052c04f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 7 Dec 2025 09:05:00 +0200 Subject: [PATCH 035/130] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 6 +- .../tap/di/domain/MarketsDomainModule.kt | 6 +- .../tap/di/domain/StakingDomainModule.kt | 6 +- .../tap/di/domain/TokensDomainModule.kt | 28 ++--- .../ui/tokens/TokenItemStateConverter.kt | 17 +-- ...onverter.kt => StakingBalanceConverter.kt} | 16 ++- .../data/staking/DefaultStakingRepository.kt | 16 +-- .../ethpool/P2PStakingBalanceConverter.kt | 60 ++++++++++ .../staking/di/StakingBalanceFetcherModule.kt | 24 ++++ .../di/StakingBalanceProducerFactoryModule.kt | 28 +++++ ...ule.kt => StakingBalanceSupplierModule.kt} | 38 +++---- .../data/staking/di/StakingDataModule.kt | 10 +- .../staking/di/YieldBalanceFetcherModule.kt | 24 ---- .../di/YieldBalanceProducerFactoryModule.kt | 28 ----- ...t => DefaultMultiStakingBalanceFetcher.kt} | 41 ++++--- ... => DefaultMultiStakingBalanceProducer.kt} | 30 ++--- .../DefaultSingleStakingBalanceFetcher.kt | 27 +++++ ...=> DefaultSingleStakingBalanceProducer.kt} | 44 ++++---- .../DefaultSingleYieldBalanceFetcher.kt | 27 ----- .../staking/store/DefaultP2PBalancesStore.kt | 62 ++++------- ...tore.kt => DefaultStakingBalancesStore.kt} | 32 +++--- .../data/staking/store/P2PBalancesStore.kt | 8 +- ...lancesStore.kt => StakingBalancesStore.kt} | 24 +--- .../staking/utils/DefaultStakingCleaner.kt | 8 +- .../tangem/data/staking/StakingBalanceExt.kt | 25 ++--- ... DefaultMultiStakingBalanceFetcherTest.kt} | 104 +++++++++--------- ...DefaultMultiStakingBalanceProducerTest.kt} | 61 +++++----- ...DefaultSingleStakingBalanceFetcherTest.kt} | 34 +++--- ...efaultSingleStakingBalanceProducerTest.kt} | 38 +++---- ...t => StakingBalancesStoreGetMethodTest.kt} | 14 +-- ...StakingBalancesStoreInitializationTest.kt} | 18 +-- ... StakingBalancesStoreUpdateMethodsTest.kt} | 14 +-- .../utils/DefaultStakingCleanerTest.kt | 12 +- .../data/swap/DefaultSwapRepositoryV2.kt | 2 +- .../DefaultCurrencyChecksRepository.kt | 4 +- ...efaultGetTangemPayCurrencyStatusUseCase.kt | 2 +- detekt_baseline_report.txt | 14 +-- .../status/di/AccountStatusUseCaseModule.kt | 6 +- .../utils/CryptoCurrencyBalanceFetcher.kt | 17 +-- .../CryptoCurrencyStatusesFlowFactory.kt | 38 +++---- .../CryptoCurrencyStatusesFlowFactoryTest.kt | 24 ++-- .../account/status/utils/TokenListExt.kt | 2 +- .../managetokens/SaveManagedTokensUseCase.kt | 12 +- .../domain/markets/SaveMarketTokensUseCase.kt | 12 +- .../models/currency/CryptoCurrencyStatus.kt | 21 ++-- .../models/staking/P2PStakingAccount.kt | 37 +++++++ .../domain/models/staking/StakingBalance.kt | 101 +++++++++++++++++ domain/staking/detekt-baseline-debug.xml | 4 +- .../domain/staking/model/StakingOption.kt | 3 +- .../FetchStakingYieldBalanceUseCase.kt | 8 +- ...rams.kt => StakingBalanceFetcherParams.kt} | 12 +- ...tcher.kt => MultiStakingBalanceFetcher.kt} | 8 +- .../multi/MultiStakingBalanceProducer.kt | 17 +++ .../multi/MultiStakingBalanceSupplier.kt | 18 +++ .../multi/MultiYieldBalanceProducer.kt | 17 --- .../multi/MultiYieldBalanceSupplier.kt | 18 --- ...cher.kt => SingleStakingBalanceFetcher.kt} | 6 +- ...cer.kt => SingleStakingBalanceProducer.kt} | 10 +- .../single/SingleStakingBalanceSupplier.kt | 18 +++ .../single/SingleYieldBalanceSupplier.kt | 18 --- .../domain/staking/utils/StakingBalanceExt.kt | 89 +++++++++++++++ .../domain/staking/utils/YieldBalanceExt.kt | 39 ------- domain/tokens/detekt-baseline-debug.xml | 1 - .../tokens/AddCryptoCurrenciesUseCase.kt | 14 +-- .../tokens/FetchCurrencyStatusUseCase.kt | 8 +- .../mapper/CurrencyStatusErrorMappers.kt | 2 +- .../error/mapper/TokenListErrorMappers.kt | 2 +- .../BaseCurrencyStatusOperations.kt | 74 ++++++------- .../CachedCurrenciesStatusesOperations.kt | 53 ++++----- .../operations/CryptoCurrencyStatusFactory.kt | 89 +++++++++------ .../CurrenciesStatusesOperations.kt | 2 +- .../tokens/operations/TokenListFactory.kt | 10 +- .../operations/TotalFiatBalanceCalculator.kt | 14 +-- .../utils/CurrencyStatusProxyCreator.kt | 24 ++-- .../tokens/wallet/WalletBalanceFetcher.kt | 14 +-- .../domain/tokens/mock/MockTokensStates.kt | 6 +- .../CryptoCurrencyStatusFactoryTest.kt | 90 +++++++-------- .../operations/PriceChangeCalculatorTest.kt | 2 +- .../TotalFiatBalanceCalculatorTest.kt | 24 ++-- .../tokens/wallet/WalletBalanceFetcherTest.kt | 48 ++++---- .../supply/YieldSupplyMinAmountUseCaseTest.kt | 6 +- ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 6 +- .../utils/FeeCalculationUtilsTest.kt | 2 +- .../staking/impl/detekt-baseline-debug.xml | 2 - .../impl/presentation/model/StakingModel.kt | 10 +- .../state/converters/BalanceItemConverter.kt | 6 +- .../RewardsValidatorStateConverter.kt | 9 +- .../converters/YieldBalancesConverter.kt | 17 +-- .../SetConfirmationStateAssentTransformer.kt | 2 +- ...etConfirmationStateCompletedTransformer.kt | 2 +- ...ConfirmationStateResetAssentTransformer.kt | 2 +- .../SetInitialDataStateTransformer.kt | 2 +- ...firmationStateAssentApprovalTransformer.kt | 2 +- .../AddStakingNotificationsTransformer.kt | 2 +- .../StakingInfoNotificationsFactory.kt | 4 +- .../impl/detekt-baseline-debug.xml | 2 - ...TokenDetailsBalanceSelectStateConverter.kt | 6 +- .../TokenDetailsLoadedBalanceConverter.kt | 4 +- .../TokenDetailsStakingInfoConverter.kt | 35 ++++-- .../intents/WalletContentClickIntents.kt | 4 +- .../intents/WalletWarningsClickIntents.kt | 11 +- .../CryptoCurrencyToDraggableItemConverter.kt | 8 +- ...ryptoCurrencyToDraggableItemConverterV2.kt | 8 +- .../transformers/SetVisaInfoTransformer.kt | 2 +- 104 files changed, 1197 insertions(+), 941 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/local/token/converter/{YieldBalanceConverter.kt => StakingBalanceConverter.kt} (82%) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt rename data/staking/src/main/java/com/tangem/data/staking/di/{YieldBalanceSupplierModule.kt => StakingBalanceSupplierModule.kt} (59%) delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt rename data/staking/src/main/java/com/tangem/data/staking/multi/{DefaultMultiYieldBalanceFetcher.kt => DefaultMultiStakingBalanceFetcher.kt} (88%) rename data/staking/src/main/java/com/tangem/data/staking/multi/{DefaultMultiYieldBalanceProducer.kt => DefaultMultiStakingBalanceProducer.kt} (53%) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt rename data/staking/src/main/java/com/tangem/data/staking/single/{DefaultSingleYieldBalanceProducer.kt => DefaultSingleStakingBalanceProducer.kt} (61%) delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt rename data/staking/src/main/java/com/tangem/data/staking/store/{DefaultYieldsBalancesStore.kt => DefaultStakingBalancesStore.kt} (84%) rename data/staking/src/main/java/com/tangem/data/staking/store/{YieldsBalancesStore.kt => StakingBalancesStore.kt} (51%) rename data/staking/src/test/kotlin/com/tangem/data/staking/multi/{DefaultMultiYieldBalanceFetcherTest.kt => DefaultMultiStakingBalanceFetcherTest.kt} (67%) rename data/staking/src/test/kotlin/com/tangem/data/staking/multi/{DefaultMultiYieldBalanceProducerTest.kt => DefaultMultiStakingBalanceProducerTest.kt} (78%) rename data/staking/src/test/kotlin/com/tangem/data/staking/single/{DefaultSingleYieldBalanceFetcherTest.kt => DefaultSingleStakingBalanceFetcherTest.kt} (51%) rename data/staking/src/test/kotlin/com/tangem/data/staking/single/{DefaultSingleYieldBalanceProducerTest.kt => DefaultSingleStakingBalanceProducerTest.kt} (79%) rename data/staking/src/test/kotlin/com/tangem/data/staking/store/{YieldsBalancesStoreGetMethodTest.kt => StakingBalancesStoreGetMethodTest.kt} (84%) rename data/staking/src/test/kotlin/com/tangem/data/staking/store/{YieldsBalancesStoreInitializationTest.kt => StakingBalancesStoreInitializationTest.kt} (82%) rename data/staking/src/test/kotlin/com/tangem/data/staking/store/{YieldsBalancesStoreUpdateMethodsTest.kt => StakingBalancesStoreUpdateMethodsTest.kt} (92%) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt rename domain/staking/src/main/java/com/tangem/domain/staking/fetcher/{YieldBalanceFetcherParams.kt => StakingBalanceFetcherParams.kt} (76%) rename domain/staking/src/main/java/com/tangem/domain/staking/multi/{MultiYieldBalanceFetcher.kt => MultiStakingBalanceFetcher.kt} (75%) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceProducer.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt rename domain/staking/src/main/java/com/tangem/domain/staking/single/{SingleYieldBalanceFetcher.kt => SingleStakingBalanceFetcher.kt} (71%) rename domain/staking/src/main/java/com/tangem/domain/staking/single/{SingleYieldBalanceProducer.kt => SingleStakingBalanceProducer.kt} (62%) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index c4e44fe950..b1d7b0a206 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -6,7 +6,7 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -74,7 +74,7 @@ internal object ManageTokensDomainModule { derivationsRepository: DerivationsRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): SaveManagedTokensUseCase { @@ -85,7 +85,7 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), ) 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 c3b9c168e1..ef41aa0007 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 @@ -10,7 +10,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -70,7 +70,7 @@ object MarketsDomainModule { currenciesRepository: CurrenciesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): SaveMarketTokensUseCase { @@ -81,7 +81,7 @@ object MarketsDomainModule { currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 631f9a46a0..0280493a9a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -7,8 +7,8 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.toggles.StakingFeatureToggles +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module @@ -113,11 +113,11 @@ internal object StakingDomainModule { @Provides @Singleton fun provideFetchStakingYieldBalanceUseCase( - singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + singleStakingBalanceFetcher: SingleStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, ): FetchStakingYieldBalanceUseCase { return FetchStakingYieldBalanceUseCase( - singleYieldBalanceFetcher = singleYieldBalanceFetcher, + singleStakingBalanceFetcher = singleStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, ) } 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 02b87eb3fa..7528f0f18b 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 @@ -12,11 +12,11 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations @@ -46,7 +46,7 @@ internal object TokensDomainModule { walletManagersFacade: WalletManagersFacade, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + singleStakingBalanceFetcher: SingleStakingBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): AddCryptoCurrenciesUseCase { @@ -55,7 +55,7 @@ internal object TokensDomainModule { walletManagersFacade = walletManagersFacade, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleYieldBalanceFetcher = singleYieldBalanceFetcher, + singleStakingBalanceFetcher = singleStakingBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, ) @@ -153,7 +153,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + singleStakingBalanceFetcher: SingleStakingBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): FetchCurrencyStatusUseCase { @@ -161,7 +161,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleYieldBalanceFetcher = singleYieldBalanceFetcher, + singleStakingBalanceFetcher = singleStakingBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, ) @@ -344,8 +344,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + singleStakingBalanceSupplier: SingleStakingBalanceSupplier, + multiStakingBalanceSupplier: MultiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { @@ -355,8 +355,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, + singleStakingBalanceSupplier = singleStakingBalanceSupplier, + multiStakingBalanceSupplier = multiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, ) @@ -376,7 +376,7 @@ internal object TokensDomainModule { multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { @@ -386,7 +386,7 @@ internal object TokensDomainModule { multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 9817a2f88a..d1f0c27dbd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -21,7 +21,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.isStakingSupported import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance @@ -159,7 +159,7 @@ class TokenItemStateConverter( return totalAmount.format { crypto(currency) } } - private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data) + private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data) ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero() private fun createTitleState( @@ -264,12 +264,13 @@ class TokenItemStateConverter( val validators = stakingApyMap[stakingKey] ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) - val yieldBalance = currencyStatus.value.yieldBalance - val hasStakedBalance = yieldBalance is YieldBalance.Data + val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data + val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit - val rateInfo: Pair? = if (hasStakedBalance) { + val rateInfo: Pair? = if (stakeKitBalance != null) { + // StakeKit-specific: try to find rate from validator address val validatorsByAddress = validators.associateBy { it.address } - yieldBalance.balance.items + stakeKitBalance.balance.items .mapNotNull { it.validatorAddress } .firstNotNullOfOrNull { address -> val validator = validatorsByAddress[address] @@ -284,6 +285,8 @@ class TokenItemStateConverter( } .maxByOrNull { it.first } } else { + // P2P or no balance: use preferred validators + // TODO p2p validators .filter { it.preferred } .mapNotNull { validator -> @@ -296,7 +299,7 @@ class TokenItemStateConverter( return StakingLocalInfo( rate = rateInfo?.first, - isActive = hasStakedBalance, + isActive = stakingBalance != null, rewardType = rateInfo?.second, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt index 828449590f..9cb1a6655c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt @@ -4,28 +4,32 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.models.StatusSource import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.utils.converter.Converter import kotlinx.datetime.Instant -class YieldBalanceConverter( +/** + * Converts StakeKit DTO to [StakingBalance]. + * Returns [StakingBalance.Data.StakeKit] for non-empty balances, [StakingBalance.Empty] otherwise. + */ +class StakingBalanceConverter( private val source: StatusSource, -) : Converter { +) : Converter { constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL) - override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? { + override fun convert(value: YieldBalanceWrapperDTO): StakingBalance? { val stakingId = StakingID( integrationId = value.integrationId ?: return null, address = value.addresses.address, ) return if (value.balances.isEmpty()) { - YieldBalance.Empty(stakingId = stakingId, source = source) + StakingBalance.Empty(stakingId = stakingId, source = source) } else { - YieldBalance.Data( + StakingBalance.Data.StakeKit( stakingId = stakingId, balance = YieldBalanceItem( items = value.balances diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index ee0a73cf7b..2d73b39384 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -3,11 +3,11 @@ package com.tangem.data.staking import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitRepository: StakeKitRepository, private val p2pEthPoolRepository: P2PEthPoolRepository, - private val stakingBalanceStoreV2: YieldsBalancesStore, + private val stakingBalanceStoreV2: StakingBalancesStore, private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, @@ -106,13 +106,13 @@ internal class DefaultStakingRepository( return withContext(dispatchers.default) { val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false - val hasDataYieldBalance by lazy { - balances.any { yieldBalance -> - (yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true + val hasDataStakingBalance by lazy { + balances.any { stakingBalance -> + stakingBalance is StakingBalance.Data } } - balances.isNotEmpty() && hasDataYieldBalance + balances.isNotEmpty() && hasDataStakingBalance } } @@ -135,7 +135,7 @@ internal class DefaultStakingRepository( address = address, ), ) - if (balance != null && balance is YieldBalance.Data && balance.balance.items.isNotEmpty()) { + if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) { return true } else { stakingFeatureToggles.isCardanoStakingEnabled diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt new file mode 100644 index 0000000000..83f9d0d2f7 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.data.staking.converters.ethpool + +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.P2PEthPoolExitRequestDTO +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* +import com.tangem.domain.staking.model.StakingIntegrationID +import kotlinx.datetime.Instant + +/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */ +internal object P2PStakingBalanceConverter { + + fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P { + val stakingId = StakingID( + integrationId = StakingIntegrationID.P2P.EthereumPooled.value, + address = response.delegatorAddress, + ) + + val account = P2PStakingAccount( + delegatorAddress = response.delegatorAddress, + vaultAddress = response.vaultAddress, + stake = convertStake(response.stake), + availableToUnstake = response.availableToUnstake, + availableToWithdraw = response.availableToWithdraw, + exitQueue = convertExitQueue(response.exitQueue), + ) + + return StakingBalance.Data.P2P( + stakingId = stakingId, + source = source, + account = account, + ) + } + + private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake { + return P2PStake( + assets = dto.assets, + totalEarnedAssets = dto.totalEarnedAssets, + ) + } + + private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue { + return P2PExitQueue( + total = dto.total.toBigDecimal(), + requests = dto.requests.map(::convertExitRequest), + ) + } + + private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest { + return P2PExitRequest( + ticket = dto.ticket, + totalAssets = dto.totalAssets.toBigDecimal(), + timestamp = Instant.fromEpochSeconds(dto.timestamp), + withdrawalTimestamp = Instant.fromEpochSeconds(dto.withdrawalTimestamp), + isClaimable = dto.isClaimable, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt new file mode 100644 index 0000000000..958f675654 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiStakingBalanceFetcher +import com.tangem.data.staking.single.DefaultSingleStakingBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingBalanceFetcherModule { + + @Binds + @Singleton + fun bindSingleStakingBalanceFetcher(impl: DefaultSingleStakingBalanceFetcher): SingleStakingBalanceFetcher + + @Binds + @Singleton + fun bindMultiStakingBalanceFetcher(impl: DefaultMultiStakingBalanceFetcher): MultiStakingBalanceFetcher +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt new file mode 100644 index 0000000000..48d46bc193 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiStakingBalanceProducer +import com.tangem.data.staking.single.DefaultSingleStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingBalanceProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleStakingBalanceProducerFactory( + impl: DefaultSingleStakingBalanceProducer.Factory, + ): SingleStakingBalanceProducer.Factory + + @Binds + @Singleton + fun bindMultiStakingBalanceProducerFactory( + impl: DefaultMultiStakingBalanceProducer.Factory, + ): MultiStakingBalanceProducer.Factory +} \ 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/StakingBalanceSupplierModule.kt similarity index 59% rename from data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt rename to data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt index 69c635bb0c..26afc00827 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/StakingBalanceSupplierModule.kt @@ -2,19 +2,17 @@ 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.DefaultStakingBalancesStore import com.tangem.data.staking.store.P2PBalancesStore -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore 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.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runSuspendCatching import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,15 +21,15 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object YieldBalanceSupplierModule { +internal object StakingBalanceSupplierModule { @Provides @Singleton - fun provideYieldsBalancesStore( + fun provideStakingBalancesStore( persistenceStore: DataStore>>, dispatchers: CoroutineDispatcherProvider, - ): YieldsBalancesStore { - return DefaultYieldsBalancesStore( + ): StakingBalancesStore { + return DefaultStakingBalancesStore( runtimeStore = RuntimeSharedStore(), persistenceStore = persistenceStore, dispatchers = dispatchers, @@ -42,25 +40,25 @@ internal object YieldBalanceSupplierModule { @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 { - return object : SingleYieldBalanceSupplier( + fun provideSingleStakingBalanceSupplier( + factory: SingleStakingBalanceProducer.Factory, + ): SingleStakingBalanceSupplier { + return object : SingleStakingBalanceSupplier( factory = factory, keyCreator = { params -> listOf( - "single_yield_balance", + "single_staking_balance", params.userWalletId.stringValue, params.stakingId.integrationId, params.stakingId.address, @@ -72,10 +70,10 @@ internal object YieldBalanceSupplierModule { @Provides @Singleton - fun provideMultiYieldBalanceSupplier(factory: MultiYieldBalanceProducer.Factory): MultiYieldBalanceSupplier { - return object : MultiYieldBalanceSupplier( + fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier { + return object : MultiStakingBalanceSupplier( factory = factory, - keyCreator = { "multi_yields_balances_${it.userWalletId.stringValue}" }, + keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" }, ) {} } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 92c1ecf98d..22164269fd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -5,7 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.staking.* import com.tangem.data.staking.converters.error.StakeKitErrorConverter -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles import com.tangem.data.staking.utils.DefaultStakingCleaner import com.tangem.datasource.api.ethpool.P2PEthPoolApi @@ -55,7 +55,7 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitRepository: StakeKitRepository, p2pEthPoolRepository: P2PEthPoolRepository, - yieldsBalancesStore: YieldsBalancesStore, + stakingBalancesStore: StakingBalancesStore, dispatchers: CoroutineDispatcherProvider, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, @@ -64,7 +64,7 @@ internal object StakingDataModule { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, p2pEthPoolRepository = p2pEthPoolRepository, - stakingBalanceStoreV2 = yieldsBalancesStore, + stakingBalanceStoreV2 = stakingBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, walletManagersFacade = walletManagersFacade, @@ -134,11 +134,11 @@ internal object StakingDataModule { @Provides @Singleton fun provideStakingCleaner( - yieldsBalancesStore: YieldsBalancesStore, + stakingBalancesStore: StakingBalancesStore, dispatchers: CoroutineDispatcherProvider, ): StakingCleaner { return DefaultStakingCleaner( - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, dispatchers = dispatchers, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt deleted file mode 100644 index 9224d037e3..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.data.staking.di - -import com.tangem.data.staking.multi.DefaultMultiYieldBalanceFetcher -import com.tangem.data.staking.single.DefaultSingleYieldBalanceFetcher -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface YieldBalanceFetcherModule { - - @Binds - @Singleton - fun bindSingleYieldBalanceFetcher(impl: DefaultSingleYieldBalanceFetcher): SingleYieldBalanceFetcher - - @Binds - @Singleton - fun bindMultiYieldBalanceFetcher(impl: DefaultMultiYieldBalanceFetcher): MultiYieldBalanceFetcher -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt deleted file mode 100644 index 5f737af9ee..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.data.staking.di - -import com.tangem.data.staking.multi.DefaultMultiYieldBalanceProducer -import com.tangem.data.staking.single.DefaultSingleYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface YieldBalanceProducerFactoryModule { - - @Binds - @Singleton - fun bindSingleYieldBalanceProducerFactory( - impl: DefaultSingleYieldBalanceProducer.Factory, - ): SingleYieldBalanceProducer.Factory - - @Binds - @Singleton - fun bindMultiYieldBalanceProducerFactory( - impl: DefaultMultiYieldBalanceProducer.Factory, - ): MultiYieldBalanceProducer.Factory -} \ No newline at end of file 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/DefaultMultiStakingBalanceFetcher.kt similarity index 88% rename from data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt rename to data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index a7bcd2cdbb..99395f26b0 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/DefaultMultiStakingBalanceFetcher.kt @@ -6,7 +6,7 @@ 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.store.StakingBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.P2PEthPoolApi @@ -25,7 +25,7 @@ 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.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.async @@ -36,13 +36,13 @@ import timber.log.Timber import javax.inject.Inject /** - * Default implementation of [MultiYieldBalanceFetcher] + * Default implementation of [MultiStakingBalanceFetcher] * * Supports both StakeKit and P2P staking providers. * * @property userWalletsStore user wallets store * @property stakingYieldsStore staking yields store - * @property yieldsBalancesStore yields balances store (StakeKit) + * @property stakingBalancesStore staking balances store (StakeKit) * @property p2pBalancesStore P2P balances store * @property stakeKitApi stake kit API * @property p2pApi P2P ETH Pool API @@ -52,19 +52,19 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ @Suppress("LongParameterList") -internal class DefaultMultiYieldBalanceFetcher @Inject constructor( +internal class DefaultMultiStakingBalanceFetcher @Inject constructor( private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, - private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingBalancesStore: StakingBalancesStore, private val p2pBalancesStore: P2PBalancesStore, private val stakeKitApi: StakeKitApi, private val p2pApi: P2PEthPoolApi, private val p2pVaultsStore: P2PEthPoolVaultsStore, private val dispatchers: CoroutineDispatcherProvider, -) : MultiYieldBalanceFetcher { +) : MultiStakingBalanceFetcher { - override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either { - Timber.i("Start fetching yield balances for params:\n$params") + override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either { + Timber.i("Start fetching staking balances for params:\n$params") val stakingIds = params.stakingIds.ifEmpty { Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") @@ -102,10 +102,13 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( } } .onLeft { throwable -> - Timber.e(throwable, "Unable to fetch yield balances $params") + Timber.e(throwable, "Unable to fetch staking balances $params") if (stakeKitIds.isNotEmpty()) { - yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakeKitIds.toSet()) + stakingBalancesStore.storeError( + userWalletId = params.userWalletId, + stakingIds = stakeKitIds.toSet(), + ) } if (p2pIds.isNotEmpty()) { p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet()) @@ -114,7 +117,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( } private suspend fun fetchStakeKitBalances(userWalletId: UserWalletId, stakingIds: Set) { - yieldsBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + stakingBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) val availableStakingIds = getAvailableStakingIds( userWalletId = userWalletId, @@ -252,7 +255,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( ) if (unavailableStakingIds.isNotEmpty()) { - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet()) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet()) } return availableStakingIds.toSet().ifEmpty { @@ -301,8 +304,10 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( .toSet() } - Timber.i("Successfully fetched yield balances for $userWalletId:\n${yieldBalances.joinToString("\n")}") - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) + Timber.i( + "Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}", + ) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) if (!allResponsesReceived(requests, yieldBalances)) { val values = stakingIds.filter { stakingId -> @@ -312,13 +317,13 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( } } - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet()) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet()) } }, onError = { throwable -> - Timber.e(throwable, "Unable to fetch yield balances $userWalletId") + Timber.e(throwable, "Unable to fetch staking balances $userWalletId") - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) throw throwable }, 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/DefaultMultiStakingBalanceProducer.kt similarity index 53% rename from data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt rename to data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducer.kt index 9d4f4917b0..2ab3c5ad8c 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/DefaultMultiStakingBalanceProducer.kt @@ -3,9 +3,9 @@ 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 +import com.tangem.data.staking.store.StakingBalancesStore +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -17,28 +17,28 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEmpty /** - * Default implementation of [MultiYieldBalanceProducer] + * Default implementation of [MultiStakingBalanceProducer] * - * Combines yield balances from both StakeKit and P2P providers. + * Combines staking balances from both StakeKit and P2P providers. * * @property params params - * @property yieldsBalancesStore StakeKit yields balances store + * @property stakingBalancesStore StakeKit staking balances store * @property p2pBalancesStore P2P balances store * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ -internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( - @Assisted val params: MultiYieldBalanceProducer.Params, - private val yieldsBalancesStore: YieldsBalancesStore, +internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor( + @Assisted val params: MultiStakingBalanceProducer.Params, + private val stakingBalancesStore: StakingBalancesStore, private val p2pBalancesStore: P2PBalancesStore, private val dispatchers: CoroutineDispatcherProvider, -) : MultiYieldBalanceProducer { +) : MultiStakingBalanceProducer { - override val fallback: Option> = emptySet().some() + override val fallback: Option> = emptySet().some() - override fun produce(): Flow> { - val stakeKitFlow = yieldsBalancesStore.get(userWalletId = params.userWalletId) + override fun produce(): Flow> { + val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId) val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId) return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances -> @@ -50,7 +50,7 @@ internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( } @AssistedFactory - interface Factory : MultiYieldBalanceProducer.Factory { - override fun create(params: MultiYieldBalanceProducer.Params): DefaultMultiYieldBalanceProducer + interface Factory : MultiStakingBalanceProducer.Factory { + override fun create(params: MultiStakingBalanceProducer.Params): DefaultMultiStakingBalanceProducer } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt new file mode 100644 index 0000000000..52742732b9 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt @@ -0,0 +1,27 @@ +package com.tangem.data.staking.single + +import arrow.core.Either +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher +import javax.inject.Inject + +/** + * Default implementation of [SingleStakingBalanceFetcher] + * + * @property multiStakingBalanceFetcher multi staking balance fetcher + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleStakingBalanceFetcher @Inject constructor( + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, +) : SingleStakingBalanceFetcher { + + override suspend fun invoke(params: SingleStakingBalanceFetcher.Params): Either { + return multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params( + userWalletId = params.userWalletId, + stakingIds = setOf(params.stakingId), + ), + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt similarity index 61% rename from data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt rename to data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt index 3dccb55dfc..2bde73daf2 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt @@ -4,10 +4,10 @@ import arrow.core.Option import arrow.core.some import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.domain.models.staking.YieldBalance -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.models.staking.StakingBalance +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.assisted.Assisted @@ -20,29 +20,29 @@ import kotlinx.coroutines.flow.mapNotNull import timber.log.Timber /** - * Default implementation of [SingleYieldBalanceProducer] + * Default implementation of [SingleStakingBalanceProducer] * - * @property params params - * @property multiYieldBalanceSupplier multi yield balance supplier - * @property analyticsExceptionHandler analytics exception handler - * @property dispatchers dispatchers + * @property params params + * @property multiStakingBalanceSupplier multi staking balance supplier + * @property analyticsExceptionHandler analytics exception handler + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ -internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( - @Assisted private val params: SingleYieldBalanceProducer.Params, - private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, +internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( + @Assisted private val params: SingleStakingBalanceProducer.Params, + private val multiStakingBalanceSupplier: MultiStakingBalanceSupplier, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, -) : SingleYieldBalanceProducer { +) : SingleStakingBalanceProducer { - override val fallback: Option = YieldBalance.Error(stakingId = params.stakingId).some() + override val fallback: Option = StakingBalance.Error(stakingId = params.stakingId).some() - override fun produce(): Flow { - Timber.i("Producing yield balance for params:\n$params") + override fun produce(): Flow { + Timber.i("Producing staking balance for params:\n$params") - return multiYieldBalanceSupplier( - params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), + return multiStakingBalanceSupplier( + params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> val currentStakingId = params.stakingId @@ -65,7 +65,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( currentBalances.joinToString("\n"), ) - val dataIndex = currentBalances.indexOfFirstOrNull { it is YieldBalance.Data } + val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data } if (dataIndex != null) { currentBalances[dataIndex] @@ -75,7 +75,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( } else { val balance = currentBalances.firstOrNull() ?: return@mapNotNull null - Timber.i("Yield balance found for $currentStakingId:\n$balance") + Timber.i("Staking balance found for $currentStakingId:\n$balance") balance } } @@ -84,7 +84,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( } @AssistedFactory - interface Factory : SingleYieldBalanceProducer.Factory { - override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer + interface Factory : SingleStakingBalanceProducer.Factory { + override fun create(params: SingleStakingBalanceProducer.Params): DefaultSingleStakingBalanceProducer } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt deleted file mode 100644 index 1666b20e35..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.data.staking.single - -import arrow.core.Either -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import javax.inject.Inject - -/** - * Default implementation of [MultiYieldBalanceFetcher] - * - * @property multiYieldBalanceFetcher multi yield balance fetcher - * -[REDACTED_AUTHOR] - */ -internal class DefaultSingleYieldBalanceFetcher @Inject constructor( - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, -) : SingleYieldBalanceFetcher { - - override suspend fun invoke(params: SingleYieldBalanceFetcher.Params): Either { - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = params.userWalletId, - stakingIds = setOf(params.stakingId), - ), - ) - } -} \ No newline at end of file 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 index 24ff66cec1..d95a730a9f 100644 --- 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 @@ -1,15 +1,14 @@ 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.data.staking.converters.ethpool.P2PStakingBalanceConverter 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.StakingBalance 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.domain.staking.model.StakingIntegrationID import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.CoroutineScope @@ -19,25 +18,22 @@ 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 WalletIdWithP2PStakingBalances = Map> internal typealias WalletIdWithP2PResponses = Map> /** * Default implementation of [P2PBalancesStore] * - * Stores P2P ETH Pool staking balances with persistence support. + * Stores P2P ETH Pool staking balances. * * @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 runtimeStore: RuntimeSharedStore, private val persistenceStore: DataStore, - private val vaultsProvider: suspend () -> List, dispatchers: CoroutineDispatcherProvider, ) : P2PBalancesStore { @@ -46,19 +42,13 @@ internal class DefaultP2PBalancesStore( 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, + val value = responses.map { response -> + P2PStakingBalanceConverter.convert( + response = response, source = StatusSource.CACHE, ) }.toSet() @@ -69,17 +59,17 @@ internal class DefaultP2PBalancesStore( } } - override fun get(userWalletId: UserWalletId): Flow> { + override fun get(userWalletId: UserWalletId): Flow> { return runtimeStore.get().map { it[userWalletId].orEmpty() } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? { return runtimeStore.getSyncOrNull() ?.get(userWalletId) ?.firstOrNull { it.stakingId == stakingId } } - override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { return runtimeStore.getSyncOrNull()?.get(userWalletId) } @@ -104,7 +94,7 @@ internal class DefaultP2PBalancesStore( updateInRuntime( userWalletId = userWalletId, stakingIds = stakingIds, - ifNotFound = ::createErrorYieldBalance, + ifNotFound = ::createErrorStakingBalance, update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, ) } @@ -117,20 +107,9 @@ internal class DefaultP2PBalancesStore( } 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, + val newBalances = values.map { response -> + P2PStakingBalanceConverter.convert( + response = response, source = StatusSource.ACTUAL, ) }.toSet() @@ -173,8 +152,7 @@ internal class DefaultP2PBalancesStore( current.toMutableMap().apply { this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty() .filterNot { response -> - val responseIntegrationId = "p2p-ethereum-pooled:${response.vaultAddress}" - responseIntegrationId in integrationIds + StakingIntegrationID.P2P.EthereumPooled.value in integrationIds } .toSet() } @@ -184,8 +162,8 @@ internal class DefaultP2PBalancesStore( private suspend fun updateInRuntime( userWalletId: UserWalletId, stakingIds: Set, - ifNotFound: (StakingID) -> YieldBalance? = { null }, - update: (YieldBalance) -> YieldBalance, + ifNotFound: (StakingID) -> StakingBalance? = { null }, + update: (StakingBalance) -> StakingBalance, ) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { @@ -209,5 +187,5 @@ internal class DefaultP2PBalancesStore( } } - private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) + private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt similarity index 84% rename from data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt rename to data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt index fa753503be..b2b0ba10a9 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt @@ -3,10 +3,10 @@ package com.tangem.data.staking.store import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.datasource.local.token.converter.StakingBalanceConverter import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace @@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch internal typealias WalletIdWithWrappers = Map> -internal typealias WalletIdWithBalances = Map> +internal typealias WalletIdWithStakingBalances = Map> /** - * Default implementation of [YieldsBalancesStore] + * Default implementation of [StakingBalancesStore] * * @property runtimeStore runtime store * @property persistenceStore persistence store @@ -30,11 +30,11 @@ internal typealias WalletIdWithBalances = Map> * [REDACTED_AUTHOR] */ -internal class DefaultYieldsBalancesStore( - private val runtimeStore: RuntimeSharedStore, +internal class DefaultStakingBalancesStore( + private val runtimeStore: RuntimeSharedStore, private val persistenceStore: DataStore, dispatchers: CoroutineDispatcherProvider, -) : YieldsBalancesStore { +) : StakingBalancesStore { private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) @@ -45,7 +45,7 @@ internal class DefaultYieldsBalancesStore( runtimeStore.store( value = cachedStatuses.map { (stringWalletId, wrappers) -> val key = UserWalletId(stringWalletId) - val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers) + val value = StakingBalanceConverter(isCached = true).convertSet(input = wrappers) .filterNotNull() .toSet() @@ -56,17 +56,17 @@ internal class DefaultYieldsBalancesStore( } } - override fun get(userWalletId: UserWalletId): Flow> { + override fun get(userWalletId: UserWalletId): Flow> { return runtimeStore.get().map { it[userWalletId].orEmpty() } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? { return runtimeStore.getSyncOrNull() ?.get(userWalletId) ?.firstOrNull { it.stakingId == stakingId } } - override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { return runtimeStore.getSyncOrNull()?.get(userWalletId) } @@ -91,7 +91,7 @@ internal class DefaultYieldsBalancesStore( updateInRuntime( userWalletId = userWalletId, stakingIds = stakingIds, - ifNotFound = ::createErrorYieldBalance, + ifNotFound = ::createErrorStakingBalance, update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, ) } @@ -107,7 +107,7 @@ internal class DefaultYieldsBalancesStore( } private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { - val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) + val newBalances = StakingBalanceConverter(isCached = false).convertSet(input = values) .filterNotNull() .toSet() @@ -140,8 +140,8 @@ internal class DefaultYieldsBalancesStore( private suspend fun updateInRuntime( userWalletId: UserWalletId, stakingIds: Set, - ifNotFound: (StakingID) -> YieldBalance? = { null }, - update: (YieldBalance) -> YieldBalance, + ifNotFound: (StakingID) -> StakingBalance? = { null }, + update: (StakingBalance) -> StakingBalance, ) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { @@ -165,7 +165,7 @@ internal class DefaultYieldsBalancesStore( } } - private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) + private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id) private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? { val integrationId = integrationId 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 index 7dbf4b1abe..02479075c3 100644 --- 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 @@ -1,8 +1,8 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow @@ -11,11 +11,11 @@ import kotlinx.coroutines.flow.Flow */ interface P2PBalancesStore { - fun get(userWalletId: UserWalletId): Flow> + fun get(userWalletId: UserWalletId): Flow> - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? - suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt similarity index 51% rename from data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt rename to data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt index 10bfa99242..5d730dd5b7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt @@ -1,39 +1,27 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow -/** - * Store of [YieldBalance]'s set - * -[REDACTED_AUTHOR] - */ -interface YieldsBalancesStore { +/** Store of StakeKit [StakingBalance] */ +interface StakingBalancesStore { - /** Get flow of [YieldBalance]'s set by [userWalletId] */ - fun get(userWalletId: UserWalletId): Flow> + fun get(userWalletId: UserWalletId): Flow> - /** Get [YieldBalance] by [userWalletId] and [stakingId] synchronously or null */ - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? - /** Get all [YieldBalance] by [userWalletId] synchronously or null */ - suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? - /** Refresh balance of [stakingId] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) - /** Refresh balances of [stakingIds] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) - /** Store actual [values] by [userWalletId] */ suspend fun storeActual(userWalletId: UserWalletId, values: Set) - /** Store error by [userWalletId] and [stakingIds] */ suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) - /** Clear balances of [stakingIds] by [userWalletId] */ suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt index 1aa6753150..c55bf2154e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt @@ -1,6 +1,6 @@ package com.tangem.data.staking.utils -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.utils.StakingCleaner @@ -9,13 +9,13 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider /** * Default implementation of [StakingCleaner]. * - * @property yieldsBalancesStore Store to manage yields balances. + * @property stakingBalancesStore Store to manage staking balances. * @property dispatchers Coroutine dispatchers provider. * [REDACTED_AUTHOR] */ internal class DefaultStakingCleaner( - private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingBalancesStore: StakingBalancesStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingCleaner { @@ -23,7 +23,7 @@ internal class DefaultStakingCleaner( if (stakingIds.isEmpty()) return with(dispatchers.default) { - yieldsBalancesStore.clear(userWalletId, stakingIds) + stakingBalancesStore.clear(userWalletId, stakingIds) } } } \ 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 index 968fb21321..a0bacfe34f 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt @@ -1,28 +1,19 @@ 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.data.staking.converters.ethpool.P2PStakingBalanceConverter 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.datasource.local.token.converter.StakingBalanceConverter import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import com.tangem.domain.models.staking.StakingBalance -internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { - return YieldBalanceConverter(source = source).convert(this)!! +internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance { + return StakingBalanceConverter(isCached = source == StatusSource.CACHE).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, +internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P { + return P2PStakingBalanceConverter.convert( + response = this, source = source, ) } \ 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/DefaultMultiStakingBalanceFetcherTest.kt similarity index 67% rename from data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index 3e0c59cd8b..e380157061 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/DefaultMultiStakingBalanceFetcherTest.kt @@ -5,7 +5,7 @@ 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.store.StakingBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -17,7 +17,7 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -31,20 +31,20 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultMultiYieldBalanceFetcherTest { +internal class DefaultMultiStakingBalanceFetcherTest { private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() - private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true) + private val stakingBalancesStore: StakingBalancesStore = 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( + private val fetcher = DefaultMultiStakingBalanceFetcher( userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, p2pBalancesStore = p2pBalancesStore, stakeKitApi = stakeKitApi, p2pApi = p2pApi, @@ -54,13 +54,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi) + clearMocks(userWalletsStore, stakingYieldsStore, stakingBalancesStore, stakeKitApi) } @Test - fun `fetch yields balances successfully`() = runTest { + fun `fetch staking balances successfully`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -81,21 +81,21 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result) } - coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } + coVerify(inverse = true) { stakingBalancesStore.storeError(any(), any()) } assertEitherRight(actual) } @Test - fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest { + fun `fetch staking balances successfully if one of stakingIds is unavailable`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -113,20 +113,20 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result) } assertEitherRight(actual) } @Test - fun `fetch yields balances failure if user wallet is not supported`() = runTest { + fun `fetch staking balances failure if user wallet is not supported`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -138,11 +138,11 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) - yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any()) } val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") @@ -151,9 +151,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { + fun `fetch staking balances failure if userWalletsStore returns null`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null @@ -164,11 +164,11 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) - yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any()) } val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") @@ -177,9 +177,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { + fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null @@ -190,14 +190,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) + stakingBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -206,9 +206,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { + fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() @@ -219,14 +219,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -235,9 +235,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if yields converting is failed`() = runTest { + fun `fetch staking balances failure if yields converting is failed`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -253,14 +253,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -269,9 +269,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { + fun `fetch staking balances failure if available yields does not contain ids from params`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -284,14 +284,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException( @@ -306,9 +306,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { + fun `fetch staking balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -329,13 +329,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) } - coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) } + coVerify(inverse = true) { stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = ApiResponseError.NetworkException() 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/DefaultMultiStakingBalanceProducerTest.kt similarity index 78% rename from data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducerTest.kt index 3435a0e034..faf28b5731 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/DefaultMultiStakingBalanceProducerTest.kt @@ -4,12 +4,13 @@ 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.store.StakingBalancesStore import com.tangem.data.staking.toDomain 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.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every @@ -22,17 +23,17 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class DefaultMultiYieldBalanceProducerTest { +internal class DefaultMultiStakingBalanceProducerTest { - private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011")) + private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011")) - private val yieldsBalancesStore = mockk() + private val stakingBalancesStore = mockk() private val p2pBalancesStore = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() - private val producer = DefaultMultiYieldBalanceProducer( + private val producer = DefaultMultiStakingBalanceProducer( params = params, - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, p2pBalancesStore = p2pBalancesStore, dispatchers = dispatchers, ) @@ -46,13 +47,13 @@ internal class DefaultMultiYieldBalanceProducerTest { val networksStatusesFlow = flowOf(balances) - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } val values = getEmittedValues(flow = actual) @@ -63,15 +64,15 @@ internal class DefaultMultiYieldBalanceProducerTest { @Test fun `test that flow is updated if balances are updated`() = runTest { - val networksStatusesFlow = MutableSharedFlow>(replay = 2) + val networksStatusesFlow = MutableSharedFlow>(replay = 2) - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } // first emit @@ -104,15 +105,15 @@ internal class DefaultMultiYieldBalanceProducerTest { @Test fun `test that flow is filtered the same balance`() = runTest { - val networksStatusesFlow = MutableSharedFlow>(replay = 2) + val networksStatusesFlow = MutableSharedFlow>(replay = 2) - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } // first emit @@ -155,19 +156,19 @@ internal class DefaultMultiYieldBalanceProducerTest { } .buffer(capacity = 5) - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(emptySet())) + Truth.assertThat(values1).isEqualTo(listOf(emptySet())) innerFlow.emit(value = true) @@ -179,19 +180,19 @@ internal class DefaultMultiYieldBalanceProducerTest { @Test fun `test that flow is empty`() = runTest { - every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow() + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) } @Test @@ -199,13 +200,13 @@ internal class DefaultMultiYieldBalanceProducerTest { val stakeKitBalances = createStakeKitBalances() val p2pBalances = createP2PBalances() - every { yieldsBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } val values = getEmittedValues(flow = actual) @@ -217,15 +218,15 @@ internal class DefaultMultiYieldBalanceProducerTest { @Test fun `test that P2P balances are updated independently from StakeKit`() = runTest { val stakeKitBalances = createStakeKitBalancesWithTonOnly() - val p2pFlow = MutableSharedFlow>(replay = 2) + val p2pFlow = MutableSharedFlow>(replay = 2) - every { yieldsBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { stakingBalancesStore.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 { stakingBalancesStore.get(params.userWalletId) } verify { p2pBalancesStore.get(params.userWalletId) } // first emit - empty P2P @@ -254,24 +255,24 @@ internal class DefaultMultiYieldBalanceProducerTest { address = "0x1", ) val p2pEthereumId = StakingID( - integrationId = "p2p-ethereum-pooled", + integrationId = StakingIntegrationID.P2P.EthereumPooled.value, address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84", ) - fun createStakeKitBalances(): Set { + fun createStakeKitBalances(): Set { return setOf( MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), ) } - fun createStakeKitBalancesWithTonOnly(): Set { + fun createStakeKitBalancesWithTonOnly(): Set { return setOf( MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), ) } - fun createP2PBalances(): Set { + fun createP2PBalances(): Set { return setOf( MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain( source = StatusSource.ACTUAL, diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt similarity index 51% rename from data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt index 6722fc8d5f..9638e95898 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt @@ -5,8 +5,8 @@ import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify @@ -20,32 +20,32 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultSingleYieldBalanceFetcherTest { +internal class DefaultSingleStakingBalanceFetcherTest { - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() - private val fetcher = DefaultSingleYieldBalanceFetcher( - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + private val fetcher = DefaultSingleStakingBalanceFetcher( + multiStakingBalanceFetcher = multiStakingBalanceFetcher, ) @BeforeEach fun resetMocks() { - clearMocks(multiYieldBalanceFetcher) + clearMocks(multiStakingBalanceFetcher) } @Test - fun `fetch yield balance successfully`() = runTest { + fun `fetch staking balance successfully`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) + val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params( + val multiParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(tonId), ) val multiResult = Unit.right() - coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult + coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult // Act val actual = fetcher.invoke(params).isRight() @@ -53,26 +53,26 @@ internal class DefaultSingleYieldBalanceFetcherTest { // Assert Truth.assertThat(actual).isTrue() - coVerify { multiYieldBalanceFetcher(params = multiParams) } + coVerify { multiStakingBalanceFetcher(params = multiParams) } } @Test - fun `fetch yield balance failure`() = runTest { + fun `fetch staking balance failure`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) + val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) + val multiParams = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) val multiResult = IllegalStateException().left() - coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult + coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult // Act val actual = fetcher.invoke(params) // Assert Truth.assertThat(actual).isEqualTo(multiResult) - coVerify { multiYieldBalanceFetcher(params = multiParams) } + coVerify { multiStakingBalanceFetcher(params = multiParams) } } private companion object { diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt similarity index 79% rename from data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt index aa9e56d44a..9c42102d3e 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt @@ -4,12 +4,12 @@ import com.google.common.truth.Truth import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.data.staking.toDomain +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -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.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks @@ -26,20 +26,20 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultSingleYieldBalanceProducerTest { +internal class DefaultSingleStakingBalanceProducerTest { - private val params = SingleYieldBalanceProducer.Params( + private val params = SingleStakingBalanceProducer.Params( userWalletId = UserWalletId(stringValue = "011"), stakingId = tonId, ) - private val multiNetworkStatusSupplier = mockk() + private val multiNetworkStatusSupplier = mockk() private val analyticsExceptionHandler = mockk(relaxUnitFun = true) private val dispatchers = TestingCoroutineDispatcherProvider() - private val producer = DefaultSingleYieldBalanceProducer( + private val producer = DefaultSingleStakingBalanceProducer( params = params, - multiYieldBalanceSupplier = multiNetworkStatusSupplier, + multiStakingBalanceSupplier = multiNetworkStatusSupplier, analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = dispatchers, ) @@ -61,7 +61,7 @@ internal class DefaultSingleYieldBalanceProducerTest { ), ) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow // Act @@ -74,17 +74,17 @@ internal class DefaultSingleYieldBalanceProducerTest { } @Test - fun `flow is updated if yield balance is updated`() = runTest { + fun `flow is updated if staking balance is updated`() = runTest { // Arrange - val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - val updatedBalance = YieldBalance.Error(stakingId = tonId) + val updatedBalance = StakingBalance.Error(stakingId = tonId) // Act (first emit) multiFlow.emit(value = setOf(balance)) @@ -108,9 +108,9 @@ internal class DefaultSingleYieldBalanceProducerTest { @Test fun `flow is filtered the same status`() = runTest { // Arrange - val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() @@ -153,7 +153,7 @@ internal class DefaultSingleYieldBalanceProducerTest { } .buffer(capacity = 5) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() @@ -162,7 +162,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val actual1 = getEmittedValues(flow = producerFlow) // Assert (first emit) - val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1")) + val fallbackStatus = StakingBalance.Error(stakingId = tonId.copy(address = "0x1")) Truth.assertThat(actual1).hasSize(1) Truth.assertThat(actual1).containsExactly(fallbackStatus) @@ -184,7 +184,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiFlow = flowOf(setOf(balance)) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produce() diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt similarity index 84% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt index 09366ecbcf..8ca3111517 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt @@ -5,7 +5,7 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -15,12 +15,12 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreGetMethodTest { +internal class StakingBalancesStoreGetMethodTest { - private val runtimeStore = RuntimeSharedStore() + private val runtimeStore = RuntimeSharedStore() private val persistenceStore = MockStateDataStore(default = emptyMap()) - private val store = DefaultYieldsBalancesStore( + private val store = DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -32,7 +32,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - val expected = listOf(emptySet()) + val expected = listOf(emptySet()) Truth.assertThat(values).isEqualTo(expected) } @@ -44,7 +44,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - val expected = listOf(emptySet()) + val expected = listOf(emptySet()) Truth.assertThat(values).isEqualTo(expected) } @@ -59,7 +59,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) } @Test diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt similarity index 82% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt index 8885ab72bc..cf08a1f8e3 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt @@ -6,7 +6,7 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every @@ -18,16 +18,16 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreInitializationTest { +internal class StakingBalancesStoreInitializationTest { @Test fun `test initialization if cache store is empty`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore: DataStore = mockk() every { persistenceStore.data } returns emptyFlow() - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -38,21 +38,21 @@ internal class YieldsBalancesStoreInitializationTest { @Test fun `test initialization if cache store contains empty map`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore = MockStateDataStore(default = emptyMap()) - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) } @Test fun `test initialization if cache store is not empty`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore = MockStateDataStore(default = emptyMap()) val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() @@ -63,7 +63,7 @@ internal class YieldsBalancesStoreInitializationTest { } } - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt similarity index 92% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt index febababb02..18d1b25415 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt @@ -7,8 +7,8 @@ import com.tangem.data.staking.toDomain import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.flow.firstOrNull @@ -18,12 +18,12 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreUpdateMethodsTest { +internal class StakingBalancesStoreUpdateMethodsTest { - private val runtimeStore = RuntimeSharedStore() + private val runtimeStore = RuntimeSharedStore() private val persistenceStore = MockStateDataStore(default = emptyMap()) - private val store = DefaultYieldsBalancesStore( + private val store = DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -33,7 +33,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { fun `refresh the single id if runtime store is empty`() = runTest { store.refresh(userWalletId = userWalletId, stakingId = stakingId) - val runtimeExpected = mapOf(userWalletId to emptySet()) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) @@ -63,7 +63,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { fun `refresh the multi ids if runtime store is empty`() = runTest { store.refresh(userWalletId = userWalletId, stakingIds = stakingIds) - val runtimeExpected = mapOf(userWalletId to emptySet()) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) @@ -129,7 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( - userWalletId to setOf(YieldBalance.Error(stakingId)), + userWalletId to setOf(StakingBalance.Error(stakingId)), ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt index 32751b6fc6..630865f7ba 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt @@ -1,6 +1,6 @@ package com.tangem.data.staking.utils -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingIntegrationID @@ -16,9 +16,9 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultStakingCleanerTest { - private val yieldsBalancesStore = mockk(relaxed = true) + private val stakingBalancesStore = mockk(relaxed = true) private val cleaner = DefaultStakingCleaner( - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) private val userWalletId = UserWalletId("011") @@ -28,7 +28,7 @@ class DefaultStakingCleanerTest { @BeforeEach fun setUp() { - clearMocks(yieldsBalancesStore) + clearMocks(stakingBalancesStore) } @Test @@ -38,7 +38,7 @@ class DefaultStakingCleanerTest { // Assert coVerifyOrder { - yieldsBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) } } @@ -49,7 +49,7 @@ class DefaultStakingCleanerTest { // Assert coVerifyOrder(inverse = true) { - yieldsBalancesStore.clear(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.clear(userWalletId = any(), stakingIds = any()) } } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 343ba4d4bd..51c2a6700d 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -403,7 +403,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status ).some(), maybeQuoteStatus = quoteStatus.toOption(), - maybeYieldBalance = none(), + maybeStakingBalance = none(), ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index 8c77f51a78..774f26176a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -9,7 +9,7 @@ import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CurrencyAmount @@ -136,7 +136,7 @@ internal class DefaultCurrencyChecksRepository( val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null - val stakingBalance = balanceValue.yieldBalance as? YieldBalance.Data + val stakingBalance = balanceValue.stakingBalance as? StakingBalance.Data val stakingTotalBalance = stakingBalance?.getTotalStakingBalance( blockchainId = currencyStatus.currency.network.rawId, ).orZero() diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt index 53ca506151..eadc740e60 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt @@ -53,7 +53,7 @@ internal class DefaultGetTangemPayCurrencyStatusUseCase @Inject constructor( ), sources = CryptoCurrencyStatus.Sources(), pendingTransactions = emptySet(), - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, ), diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index f0dde73ad3..aed69f1e1f 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-03 22:08:49 +Date: 2025-12-04 17:37:53 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1385 + Total Issues: 1380 Modules with Issues: 50 Average Issues per Module: 27 Progress: - Fixed: 417 out of 1802 (23%) - Remaining: 1385 + Fixed: 422 out of 1802 (23%) + Remaining: 1380 ========================================== All Modules with Issues (sorted by count) @@ -38,18 +38,18 @@ features/swap/impl 67 features/send-v2/impl 57 data/wallet-connect 55 features/hot-wallet/impl 53 -features/tokendetails/impl 48 -features/staking/impl 48 features/walletconnect/impl 47 +features/tokendetails/impl 46 +features/staking/impl 46 features/manage-tokens/impl 45 features/swap-v2/impl 40 domain/wallets 37 features/nft/impl 34 features/tester/impl 30 core/ui 27 -domain/tokens 26 common/ui 26 features/swap/domain 25 +domain/tokens 25 data/visa 22 features/yield-supply/impl 21 features/tangempay/details/impl 21 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 32444c8e37..c2d429c650 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 @@ -12,7 +12,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -125,14 +125,14 @@ internal object AccountStatusUseCaseModule { fun provideCryptoCurrencyBalanceFetcher( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): CryptoCurrencyBalanceFetcher { return CryptoCurrencyBalanceFetcher( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), ) 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 ebd48e4b0b..d9e1e38b0d 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 @@ -6,7 +6,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.wallet.FetchingSource import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -19,7 +19,7 @@ import timber.log.Timber * * @property multiNetworkStatusFetcher Fetcher for updating network statuses. * @property multiQuoteStatusFetcher Fetcher for updating quote statuses. - * @property multiYieldBalanceFetcher Fetcher for updating yield balances. + * @property multiStakingBalanceFetcher Fetcher for updating staking balances. * @property stakingIdFactory Factory for creating staking IDs. * @property parallelUpdatingScope Coroutine scope for parallel balance updates. * @@ -28,7 +28,7 @@ import timber.log.Timber class CryptoCurrencyBalanceFetcher( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -52,7 +52,10 @@ class CryptoCurrencyBalanceFetcher( FetchingSource.NETWORK to refreshNetworks(userWalletId = userWalletId, currencies = currencies) }, async { - FetchingSource.STAKING to refreshYieldBalances(userWalletId = userWalletId, currencies = currencies) + FetchingSource.STAKING to refreshStakingBalances( + userWalletId = userWalletId, + currencies = currencies, + ) }, async { FetchingSource.QUOTE to refreshQuotes(currencies = currencies) }, ) @@ -85,7 +88,7 @@ class CryptoCurrencyBalanceFetcher( ) } - private suspend fun refreshYieldBalances( + private suspend fun refreshStakingBalances( userWalletId: UserWalletId, currencies: List, ): Either { @@ -93,8 +96,8 @@ class CryptoCurrencyBalanceFetcher( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + return multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt index cd8bb66bb6..525875e98d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -16,8 +16,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -28,7 +28,7 @@ import javax.inject.Inject * * @property singleNetworkStatusSupplier Supplier for obtaining network status. * @property singleQuoteStatusSupplier Supplier for obtaining quote status. - * @property singleYieldBalanceSupplier Supplier for obtaining yield balance. + * @property singleStakingBalanceSupplier Supplier for obtaining staking balance. * @property stakingIdFactory Factory for creating staking IDs. * [REDACTED_AUTHOR] @@ -36,7 +36,7 @@ import javax.inject.Inject internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -53,7 +53,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( currency = currency, maybeNetworkStatus = statusSources.networkStatus.toOption(), maybeQuoteStatus = statusSources.quoteStatus.toOption(), - maybeYieldBalance = statusSources.yieldBalance.toOption(), + maybeStakingBalance = statusSources.stakingBalance.toOption(), ) } .onEmpty { @@ -71,10 +71,10 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( ): Flow { val networkStatusFlow = getNetworkStatusFlow(userWalletId = userWallet.walletId, network = currency.network) - val yieldBalanceFlow = if (userWallet.isMultiCurrency) { + val stakingBalanceFlow = if (userWallet.isMultiCurrency) { networkStatusFlow.flatMapLatest { networkStatus -> if (networkStatus != null) { - getYieldBalanceFlow( + getStakingBalanceFlow( userWalletId = userWallet.walletId, currencyId = currency.id, networkStatus = networkStatus, @@ -89,26 +89,26 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( val quoteStatusFlow = currency.id.rawCurrencyId?.let(::getQuoteStatusFlow) - return combine(networkStatusFlow, yieldBalanceFlow, quoteStatusFlow) + return combine(networkStatusFlow, stakingBalanceFlow, quoteStatusFlow) .distinctUntilChanged() } private fun combine( networkStatusFlow: Flow, - yieldBalanceFlow: Flow?, + stakingBalanceFlow: Flow?, quoteStatusFlow: Flow?, ): Flow { return when { - yieldBalanceFlow != null && quoteStatusFlow != null -> { + stakingBalanceFlow != null && quoteStatusFlow != null -> { combine( flow = networkStatusFlow, - flow2 = yieldBalanceFlow, + flow2 = stakingBalanceFlow, flow3 = quoteStatusFlow, transform = ::CryptoCurrencyStatusSources, ) } - yieldBalanceFlow != null -> { - combine(flow = networkStatusFlow, flow2 = yieldBalanceFlow, transform = ::CryptoCurrencyStatusSources) + stakingBalanceFlow != null -> { + combine(flow = networkStatusFlow, flow2 = stakingBalanceFlow, transform = ::CryptoCurrencyStatusSources) } quoteStatusFlow != null -> { combine(flow = networkStatusFlow, flow2 = quoteStatusFlow) { networkStatus, quoteStatus -> @@ -136,11 +136,11 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( .distinctUntilChanged() } - private fun getYieldBalanceFlow( + private fun getStakingBalanceFlow( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, networkStatus: NetworkStatus, - ): Flow { + ): Flow { val stakingId = stakingIdFactory.create( currencyId = currencyId, defaultAddress = networkStatus.getAddress(), @@ -148,8 +148,8 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( .getOrNull() return if (stakingId != null) { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .distinctUntilChanged() } else { @@ -159,7 +159,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( private data class CryptoCurrencyStatusSources( val networkStatus: NetworkStatus? = null, - val yieldBalance: YieldBalance? = null, + val stakingBalance: StakingBalance? = null, val quoteStatus: QuoteStatus? = null, ) } \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt index a5df234f9a..65f8d8e607 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -20,8 +20,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.test.core.getEmittedValues import io.mockk.* import kotlinx.coroutines.flow.emptyFlow @@ -41,13 +41,13 @@ class CryptoCurrencyStatusesFlowFactoryTest { private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk() private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk() - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier = mockk() + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val factory = CryptoCurrencyStatusesFlowFactory( singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, + singleStakingBalanceSupplier = singleStakingBalanceSupplier, stakingIdFactory = stakingIdFactory, ) @@ -63,7 +63,7 @@ class CryptoCurrencyStatusesFlowFactoryTest { clearMocks( singleNetworkStatusSupplier, singleQuoteStatusSupplier, - singleYieldBalanceSupplier, + singleStakingBalanceSupplier, stakingIdFactory, ) } @@ -96,13 +96,13 @@ class CryptoCurrencyStatusesFlowFactoryTest { stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value) } returns stakingId.right() - val yieldBalance = YieldBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL) - val yieldBalanceFlow = flowOf(yieldBalance) + val stakingBalance = StakingBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL) + val stakingBalanceFlow = flowOf(stakingBalance) every { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) - } returns yieldBalanceFlow + } returns stakingBalanceFlow // Act val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues) @@ -121,7 +121,7 @@ class CryptoCurrencyStatusesFlowFactoryTest { coVerify(ordering = Ordering.SEQUENCE) { singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network)) stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value) - singleYieldBalanceSupplier(params = SingleYieldBalanceProducer.Params(userWalletId, stakingId)) + singleStakingBalanceSupplier(params = SingleStakingBalanceProducer.Params(userWalletId, stakingId)) } } diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt index 0745e8a650..da013e557b 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt @@ -44,7 +44,7 @@ internal fun createStatus(currency: CryptoCurrency, fiatAmount: BigDecimal): Cry fiatRate = BigDecimal.ONE, fiatAmount = fiatAmount, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, hasCurrentNetworkTransactions = false, yieldSupplyStatus = null, pendingTransactions = emptySet(), 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 aa30b2a392..3fd2900bd7 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 @@ -10,7 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -28,7 +28,7 @@ class SaveManagedTokensUseCase( private val derivationsRepository: DerivationsRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -70,7 +70,7 @@ class SaveManagedTokensUseCase( ) } launch { - refreshUpdatedYieldBalances( + refreshUpdatedStakingBalances( userWalletId = userWalletId, addedCurrencies = savedCurrencies, ) @@ -127,7 +127,7 @@ class SaveManagedTokensUseCase( ) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, addedCurrencies: List, ) { @@ -135,8 +135,8 @@ class SaveManagedTokensUseCase( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } 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 b6e00f2b78..08b7e83e21 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 @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -36,7 +36,7 @@ class SaveMarketTokensUseCase( private val currenciesRepository: CurrenciesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -85,7 +85,7 @@ class SaveMarketTokensUseCase( syncTokens(userWalletId, savedCurrencies) launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) } - launch { refreshUpdatedYieldBalances(userWalletId, savedCurrencies) } + launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) } launch { refreshUpdatedQuotes(savedCurrencies) } } } @@ -121,7 +121,7 @@ class SaveMarketTokensUseCase( ) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, existingCurrencies: List, ) { @@ -129,8 +129,8 @@ class SaveMarketTokensUseCase( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt index 0663a46f9f..967a0c2b07 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt @@ -5,7 +5,7 @@ import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.serialization.SerializedBigDecimal -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.yield.supply.YieldSupplyStatus import kotlinx.serialization.Serializable @@ -56,13 +56,10 @@ data class CryptoCurrencyStatus( /** The network address */ val networkAddress: NetworkAddress? get() = null - /** Staking yield balance */ - val yieldBalance: YieldBalance? get() = null + /** Staking balance */ + val stakingBalance: StakingBalance? get() = null - /** - * !!! DO NOT CONFUSE with STAKING YIELD BALANCE - * Yield supply status - */ + /** Yield supply status */ val yieldSupplyStatus: YieldSupplyStatus? get() = null /** Sources */ @@ -73,11 +70,11 @@ data class CryptoCurrencyStatus( data class Sources( val networkSource: StatusSource = StatusSource.ACTUAL, val quoteSource: StatusSource = StatusSource.ACTUAL, - val yieldBalanceSource: StatusSource = StatusSource.ACTUAL, + val stakingBalanceSource: StatusSource = StatusSource.ACTUAL, ) { val total: StatusSource by lazy { - listOf(networkSource, quoteSource, yieldBalanceSource).getResultStatusSource() + listOf(networkSource, quoteSource, stakingBalanceSource).getResultStatusSource() } } @@ -163,7 +160,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: SerializedBigDecimal, override val fiatRate: SerializedBigDecimal, override val priceChange: SerializedBigDecimal, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, @@ -191,7 +188,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: SerializedBigDecimal?, override val fiatRate: SerializedBigDecimal?, override val priceChange: SerializedBigDecimal?, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, @@ -213,7 +210,7 @@ data class CryptoCurrencyStatus( @Serializable data class NoQuote( override val amount: SerializedBigDecimal, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt new file mode 100644 index 0000000000..edc489e496 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models.staking + +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +/** P2P.org staking account */ +@Serializable +data class P2PStakingAccount( + val delegatorAddress: String, + val vaultAddress: String, + val stake: P2PStake, + val availableToUnstake: SerializedBigDecimal, + val availableToWithdraw: SerializedBigDecimal, + val exitQueue: P2PExitQueue, +) + +@Serializable +data class P2PStake( + val assets: SerializedBigDecimal, + val totalEarnedAssets: SerializedBigDecimal, +) + +@Serializable +data class P2PExitQueue( + val total: SerializedBigDecimal, + val requests: List, +) + +@Serializable +data class P2PExitRequest( + val ticket: String, + val totalAssets: SerializedBigDecimal, + val timestamp: Instant, + val withdrawalTimestamp: Instant, + val isClaimable: Boolean, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt new file mode 100644 index 0000000000..a8493d22f4 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt @@ -0,0 +1,101 @@ +package com.tangem.domain.models.staking + +import com.tangem.domain.models.StatusSource +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +/** + * Staking balance facade covering StakeKit and P2P balances + */ +@Serializable +sealed interface StakingBalance { + + val stakingId: StakingID + val source: StatusSource + + val totalStaked: BigDecimal + val totalRewards: BigDecimal? + val unstakingAmount: BigDecimal? + val withdrawableAmount: BigDecimal? + + @Serializable + sealed interface Data : StakingBalance { + + @Serializable + data class StakeKit( + override val stakingId: StakingID, + override val source: StatusSource, + val balance: YieldBalanceItem, + ) : Data { + + override val totalStaked: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.STAKED } + .sumOf { it.amount } + + override val totalRewards: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount } + + override val unstakingAmount: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.UNSTAKING || it.type == BalanceType.UNLOCKING } + .sumOf { it.amount } + + override val withdrawableAmount: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.UNSTAKED } + .sumOf { it.amount } + } + + @Serializable + data class P2P( + override val stakingId: StakingID, + override val source: StatusSource, + val account: P2PStakingAccount, + ) : Data { + + override val totalStaked: BigDecimal + get() = account.stake.assets + + override val totalRewards: BigDecimal + get() = account.stake.totalEarnedAssets + + override val unstakingAmount: BigDecimal + get() = account.exitQueue.total + + override val withdrawableAmount: BigDecimal + get() = account.availableToWithdraw + } + } + + @Serializable + data class Empty( + override val stakingId: StakingID, + override val source: StatusSource, + ) : StakingBalance { + override val totalStaked: BigDecimal get() = BigDecimal.ZERO + override val totalRewards: BigDecimal? get() = null + override val unstakingAmount: BigDecimal? get() = null + override val withdrawableAmount: BigDecimal? get() = null + } + + @Serializable + data class Error(override val stakingId: StakingID) : StakingBalance { + override val source: StatusSource get() = StatusSource.ACTUAL + override val totalStaked: BigDecimal get() = BigDecimal.ZERO + override val totalRewards: BigDecimal? get() = null + override val unstakingAmount: BigDecimal? get() = null + override val withdrawableAmount: BigDecimal? get() = null + } + + fun copySealed(source: StatusSource): StakingBalance { + return when (this) { + is Data.StakeKit -> copy(source = source) + is Data.P2P -> copy(source = source) + is Empty -> copy(source = source) + is Error -> this + } + } +} \ No newline at end of file diff --git a/domain/staking/detekt-baseline-debug.xml b/domain/staking/detekt-baseline-debug.xml index d84b9b39da..212e197fcc 100644 --- a/domain/staking/detekt-baseline-debug.xml +++ b/domain/staking/detekt-baseline-debug.xml @@ -7,8 +7,8 @@ MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress } NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId) NullableToStringCall:StakingApyFlowUseCase.kt$StakingApyFlowUseCase$${yield.token.coinGeckoId} - UnnecessaryAbstractClass:MultiYieldBalanceSupplier.kt$MultiYieldBalanceSupplier$MultiYieldBalanceSupplier - UnnecessaryAbstractClass:SingleYieldBalanceSupplier.kt$SingleYieldBalanceSupplier$SingleYieldBalanceSupplier + UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier + UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf() UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "" 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 d199edb836..77078dd75e 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 @@ -40,8 +40,7 @@ sealed interface StakingOption { * Wraps P2P ETH Pool vault information */ data class P2P(val vault: P2PEthPoolVault) : StakingOption { - override val integrationId: String = - "p2p-ethereum-pooled" + override val integrationId: String = "p2p-ethereum-pooled" override val apy: SerializedBigDecimal = vault.apy override val token: YieldToken = createEthToken() override val isAvailable: Boolean = !vault.isPrivate diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 0308e969e5..a438d88cef 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -7,10 +7,10 @@ import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher class FetchStakingYieldBalanceUseCase( - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, ) { @@ -32,8 +32,8 @@ class FetchStakingYieldBalanceUseCase( return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .mapLeft { StakingError.DomainError("$it") } .bind() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt similarity index 76% rename from domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt index e73c76dc8a..de89cdee9e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt @@ -5,17 +5,17 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId /** - * Params for fetchers of yield balance + * Params for fetchers of staking balance * [REDACTED_AUTHOR] */ -sealed interface YieldBalanceFetcherParams { +sealed interface StakingBalanceFetcherParams { /** User wallet ID */ val userWalletId: UserWalletId /** - * Params for fetching multiple yield balances + * Params for fetching multiple staking balances * * @property userWalletId user wallet ID * @property currencyIdWithNetworkMap map of currency ID to network @@ -23,10 +23,10 @@ sealed interface YieldBalanceFetcherParams { data class Multi( override val userWalletId: UserWalletId, val currencyIdWithNetworkMap: Map, - ) : YieldBalanceFetcherParams + ) : StakingBalanceFetcherParams /** - * Params for fetching single yield balance + * Params for fetching single staking balance * * @property userWalletId user wallet ID * @property currencyId currency ID @@ -36,5 +36,5 @@ sealed interface YieldBalanceFetcherParams { override val userWalletId: UserWalletId, val currencyId: CryptoCurrency.ID, val network: Network, - ) : YieldBalanceFetcherParams + ) : StakingBalanceFetcherParams } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt similarity index 75% rename from domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt index 041755dbc8..9657856acf 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt @@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.staking.StakingID /** - * Fetcher of yields balances + * Fetcher of staking balances * [REDACTED_AUTHOR] */ -interface MultiYieldBalanceFetcher : FlowFetcher { +interface MultiStakingBalanceFetcher : FlowFetcher { /** - * Params for fetching multiple yield balances + * Params for fetching multiple staking balances * * @property userWalletId user wallet ID * @property stakingIds map of currency ID to network @@ -24,7 +24,7 @@ interface MultiYieldBalanceFetcher : FlowFetcher> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt new file mode 100644 index 0000000000..106e390f01 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.staking.StakingBalance + +/** + * Supplier of all staking balances for selected wallet [MultiStakingBalanceProducer.Params] + * + * @property factory factory for creating [MultiStakingBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiStakingBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (MultiStakingBalanceProducer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt deleted file mode 100644 index 7b9357cb24..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.staking.multi - -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Producer of all yield balances for selected wallet [UserWalletId] - * -[REDACTED_AUTHOR] - */ -interface MultiYieldBalanceProducer : FlowProducer> { - - data class Params(val userWalletId: UserWalletId) - - interface Factory : FlowProducer.Factory -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt deleted file mode 100644 index 6d6f106113..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.staking.multi - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance - -/** - * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] - * - * @property factory factory for creating [MultiYieldBalanceProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class MultiYieldBalanceSupplier( - override val factory: FlowProducer.Factory, - override val keyCreator: (MultiYieldBalanceProducer.Params) -> String, -) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt similarity index 71% rename from domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt index 61a7ee90c6..f7ef742c1d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt @@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.staking.StakingID /** - * Fetcher of yield balance + * Fetcher of staking balance * [REDACTED_AUTHOR] */ -interface SingleYieldBalanceFetcher : FlowFetcher { +interface SingleStakingBalanceFetcher : FlowFetcher { /** - * Params for fetching single yield balance + * Params for fetching single staking balance * * @property userWalletId user wallet ID * @property stakingId staking ID diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt similarity index 62% rename from domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt index 9d33907751..5bd51edae6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt @@ -2,15 +2,15 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance /** - * Producer of yield balance for selected wallet [UserWalletId] + * Producer of staking balance for selected wallet [UserWalletId] * [REDACTED_AUTHOR] */ -interface SingleYieldBalanceProducer : FlowProducer { +interface SingleStakingBalanceProducer : FlowProducer { data class Params( val userWalletId: UserWalletId, @@ -19,7 +19,7 @@ interface SingleYieldBalanceProducer : FlowProducer { override fun toString(): String { return """ - SingleYieldBalanceProducer.Params( + SingleStakingBalanceProducer.Params( userWalletId = $userWalletId, stakingId = $stakingId, ) @@ -27,5 +27,5 @@ interface SingleYieldBalanceProducer : FlowProducer { } } - interface Factory : FlowProducer.Factory + interface Factory : FlowProducer.Factory } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt new file mode 100644 index 0000000000..9474e0e172 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.staking.StakingBalance + +/** + * Supplier of staking balance for selected wallet [SingleStakingBalanceProducer.Params] + * + * @property factory factory for creating [SingleStakingBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleStakingBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (SingleStakingBalanceProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt deleted file mode 100644 index 4d93e733a1..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.staking.single - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance - -/** - * Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params] - * - * @property factory factory for creating [SingleYieldBalanceProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class SingleYieldBalanceSupplier( - override val factory: FlowProducer.Factory, - override val keyCreator: (SingleYieldBalanceProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt new file mode 100644 index 0000000000..75b52aa6f4 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt @@ -0,0 +1,89 @@ +package com.tangem.domain.staking.utils + +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.lib.crypto.BlockchainUtils +import java.math.BigDecimal + +/** + * Provider-agnostic extension to get total balance including rewards. + * Works for both StakeKit and P2P providers. + * + * Returns sum of all staking-related balances including rewards + * (staked + unstaking + withdrawable + rewards). + * + * When [BlockchainUtils.isIncludeStakingTotalBalance] is false, the staked balance + * is already included in the main wallet balance, so we only return rewards. + */ +fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { + return when (this) { + is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId) + is StakingBalance.Data.P2P -> { + val rewards = totalRewards + if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) { + totalStaked + unstakingAmount + withdrawableAmount + rewards + } else { + rewards + } + } + } +} + +/** + * Provider-agnostic extension to get total staking balance excluding rewards. + * Works for both StakeKit and P2P providers. + * + * Returns sum of all staking-related balances (staked + unstaking + withdrawable) + * excluding rewards. + */ +fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { + return when (this) { + is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId) + is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount + } +} + +/** + * StakeKit-specific extension to get total balance including rewards. + */ +private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeKit(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items.sumOf { it.amount } + } else { + getRewardStakingBalance() + } +} + +/** + * StakeKit-specific extension to get total staked balance excluding rewards. + */ +private fun StakingBalance.Data.StakeKit.getTotalStakingBalanceStakeKit(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } + } else { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } - getRewardStakingBalance() + } +} + +/** + * StakeKit-specific extension to get reward balance. + */ +fun StakingBalance.Data.StakeKit.getRewardStakingBalance(): BigDecimal { + return balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount } +} + +/** + * StakeKit-specific extension to get validators count. + */ +fun StakingBalance.Data.StakeKit.getValidatorsCount(): Int { + return balance.items + .filterNot { it.validatorAddress.isNullOrBlank() } + .distinctBy { it.validatorAddress } + .size +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt deleted file mode 100644 index f6b6a96d42..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.staking.utils - -import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.lib.crypto.BlockchainUtils -import java.math.BigDecimal - -fun YieldBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { - balance.items.sumOf { it.amount } - } else { - getRewardStakingBalance() - } -} - -fun YieldBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { - balance.items - .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - } else { - balance.items - .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - getRewardStakingBalance() - } -} - -fun YieldBalance.Data.getRewardStakingBalance(): BigDecimal { - return balance.items - .filter { it.type == BalanceType.REWARDS } - .sumOf { it.amount } -} - -fun YieldBalance.Data.getValidatorsCount(): Int { - return balance.items - .filterNot { it.validatorAddress.isNullOrBlank() } - .distinctBy { it.validatorAddress } - .size -} \ No newline at end of file diff --git a/domain/tokens/detekt-baseline-debug.xml b/domain/tokens/detekt-baseline-debug.xml index 695177702e..34d0f3eeb3 100644 --- a/domain/tokens/detekt-baseline-debug.xml +++ b/domain/tokens/detekt-baseline-debug.xml @@ -18,7 +18,6 @@ MultilineLambdaItParameter:PriceChangeCalculator.kt$PriceChangeCalculator${ val weight = it.value.fiatAmount.orZero().divide(balance, 2, RoundingMode.HALF_UP) val priceChange = it.value.priceChange.orZero() weight * priceChange } MultilineLambdaItParameter:WalletBalanceFetcher.kt$WalletBalanceFetcher${ val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it) if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${it.id}") } stakingId } NamedArguments:ApplyTokenListSortingUseCase.kt$ApplyTokenListSortingUseCase$saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) - NamedArguments:CryptoCurrencyStatusFactory.kt$CryptoCurrencyStatusFactory$createStatus(currency, status, quoteStatus, maybeYieldBalance) NamedArguments:GetCurrencyWarningsUseCase.kt$GetCurrencyWarningsUseCase$combine( getCoinRelatedWarnings( userWalletId = userWalletId, networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource -> setOfNotNull( maybeRentWarning, maybeEdWarning?.let { getExistentialDepositWarning(currency, it) }, maybeFeeResource?.let { getFeeResourceWarning(it) }, * coinRelatedWarnings.toTypedArray(), getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), getBeaconChainShutdownWarning(rawId = currency.network.id.rawId), getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), getMigrationFromMaticToPolWarning(currency), ) } NoNameShadowing:WalletBalanceFetcher.kt$WalletBalanceFetcher${ it is StakingIdFactory.Error.UnableToGetAddress } NullableBooleanCheck:GetCurrencyCheckUseCase.kt$GetCurrencyCheckUseCase$recipientAddress?.let { currencyChecksRepository.checkIfAccountFunded( userWalletId, network, recipientAddress, ) } ?: false 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 19357b3e82..7473e2eb33 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 @@ -12,7 +12,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.async @@ -32,7 +32,7 @@ class AddCryptoCurrenciesUseCase( private val walletManagersFacade: WalletManagersFacade, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -82,7 +82,7 @@ class AddCryptoCurrenciesUseCase( awaitAll( async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, - async { refreshUpdatedYieldBalances(userWalletId, currencyToAdd) }, + async { refreshUpdatedStakingBalances(userWalletId, currencyToAdd) }, async { refreshUpdatedQuotes(currencyToAdd) }, ) } @@ -122,7 +122,7 @@ class AddCryptoCurrenciesUseCase( awaitAll( async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, - async { refreshUpdatedYieldBalances(userWalletId, tokenToAdd) }, + async { refreshUpdatedStakingBalances(userWalletId, tokenToAdd) }, async { refreshUpdatedQuotes(tokenToAdd) }, ) } @@ -177,7 +177,7 @@ class AddCryptoCurrenciesUseCase( ) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, addedCurrency: CryptoCurrency, ): Either = either { @@ -195,8 +195,8 @@ class AddCryptoCurrenciesUseCase( return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .bind() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 5c7ce2a444..a1598a3f5f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.async @@ -32,7 +32,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -149,8 +149,8 @@ class FetchCurrencyStatusUseCase( return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .bind() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 7fb8c805ca..6fc01f0c95 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError { return when (this) { is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause) - is CurrenciesStatusesOperations.Error.EmptyYieldBalances, + is CurrenciesStatusesOperations.Error.EmptyStakingBalances, is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 5e4071d5b1..081d02f18f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -12,7 +12,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.EmptyAddresses, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, - is CurrenciesStatusesOperations.Error.EmptyYieldBalances, + is CurrenciesStatusesOperations.Error.EmptyStakingBalances, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 76c726d9de..10043d51ca 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -10,8 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -22,10 +22,10 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.isStakingSupported -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.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.error.TokenListError @@ -48,8 +48,8 @@ abstract class BaseCurrencyStatusOperations( private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, + private val multiStakingBalanceSupplier: MultiStakingBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -83,7 +83,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId: UserWalletId, currency: CryptoCurrency, includeQuotes: Boolean = true, - subscribeOnYieldBalance: Boolean = true, + subscribeOnStakingBalance: Boolean = true, ): Flow> { val rawCurrencyId = currency.id.rawCurrencyId @@ -104,7 +104,7 @@ abstract class BaseCurrencyStatusOperations( val isStakingSupported = currency.network.toBlockchain().isStakingSupported - val yieldBalanceFlow = if (isStakingSupported) { + val stakingBalanceFlow = if (isStakingSupported) { val stakingId = stakingIdFactory.create( userWalletId = userWalletId, currencyId = currency.id, @@ -113,19 +113,19 @@ abstract class BaseCurrencyStatusOperations( .getOrNull() stakingId?.let { - getYieldBalance(userWalletId = userWalletId, stakingId = it) + getStakingBalance(userWalletId = userWalletId, stakingId = it) } } else { null } - return if (subscribeOnYieldBalance && yieldBalanceFlow != null) { - combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + return if (subscribeOnStakingBalance && stakingBalanceFlow != null) { + combine(quoteFlow, statusFlow, stakingBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeStakingBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) } } else { @@ -134,7 +134,7 @@ abstract class BaseCurrencyStatusOperations( currency = currency, maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = null, + maybeStakingBalance = null, ) } } @@ -209,13 +209,13 @@ abstract class BaseCurrencyStatusOperations( .firstOrNull() .right() - val yieldBalances = getYieldBalanceSync(userWalletId, currency) + val stakingBalances = getStakingBalanceSync(userWalletId, currency) return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = quote, maybeNetworkStatus = networkStatuses, - maybeYieldBalance = yieldBalances, + maybeStakingBalance = stakingBalances, ) }, catch = { raise(Error.DataError(it)) }, @@ -245,7 +245,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId = userWalletId, currency = currency, includeQuotes = includeQuotes, - subscribeOnYieldBalance = false, + subscribeOnStakingBalance = false, ) } @@ -271,13 +271,13 @@ abstract class BaseCurrencyStatusOperations( .orEmpty() .right() - val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies) + val stakingBalances = getStakingBalancesSync(userWalletId, nonEmptyCurrencies) return currencyStatusProxyCreator.createCurrenciesStatuses( currencies = nonEmptyCurrencies, maybeQuotes = quotes, maybeNetworkStatuses = networkStatuses, - maybeYieldBalances = yieldBalances, + maybeStakingBalances = stakingBalances, ) }, catch = { raise(Error.DataError(it)) }, @@ -304,13 +304,13 @@ abstract class BaseCurrencyStatusOperations( .firstOrNull() .right() - val yieldBalances = getYieldBalanceSync(userWalletId, currency) + val stakingBalances = getStakingBalanceSync(userWalletId, currency) return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = quotes, maybeNetworkStatus = networkStatus, - maybeYieldBalance = yieldBalances, + maybeStakingBalance = stakingBalances, ) } @@ -338,16 +338,16 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private fun getYieldBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { - return singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params( + private fun getStakingBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { + return singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params( userWalletId = userWalletId, stakingId = stakingId, ), ) - .map> { it.right() } + .map> { it.right() } .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } + .onEmpty { emit(Error.EmptyStakingBalances.left()) } } private fun getNetworkStatus(userWalletId: UserWalletId, network: Network): EitherFlow { @@ -389,32 +389,32 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private suspend fun getYieldBalancesSync( + private suspend fun getStakingBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Either> = either { + ): Either> = either { val stakingIds = cryptoCurrencies.mapNotNull { cryptoCurrency -> stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) .getOrNull() } - ensure(stakingIds.isNotEmpty()) { Error.EmptyYieldBalances } + ensure(stakingIds.isNotEmpty()) { Error.EmptyStakingBalances } - val balances = multiYieldBalanceSupplier.getSyncOrNull( - params = MultiYieldBalanceProducer.Params(userWalletId = userWalletId), + val balances = multiStakingBalanceSupplier.getSyncOrNull( + params = MultiStakingBalanceProducer.Params(userWalletId = userWalletId), ) .orEmpty() .filter { it.stakingId in stakingIds } - ensure(balances.isNotEmpty()) { Error.EmptyYieldBalances } + ensure(balances.isNotEmpty()) { Error.EmptyStakingBalances } balances } - private suspend fun getYieldBalanceSync( + private suspend fun getStakingBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either = either { + ): Either = either { val stakingId = stakingIdFactory.create(userWalletId, cryptoCurrency) .mapLeft { val exception = IllegalStateException("$it") @@ -422,14 +422,14 @@ abstract class BaseCurrencyStatusOperations( } .bind() - val yieldBalance = singleYieldBalanceSupplier.getSyncOrNull( - params = SingleYieldBalanceProducer.Params( + val yieldBalance = singleStakingBalanceSupplier.getSyncOrNull( + params = SingleStakingBalanceProducer.Params( userWalletId = userWalletId, stakingId = stakingId, ), ) - ensureNotNull(yieldBalance) { Error.EmptyYieldBalances } + ensureNotNull(yieldBalance) { Error.EmptyStakingBalances } } private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index fb20d04a34..a1561d6d2e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -16,7 +16,7 @@ import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer @@ -26,9 +26,9 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -46,8 +46,8 @@ class CachedCurrenciesStatusesOperations( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, + multiStakingBalanceSupplier: MultiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) : BaseCurrencyStatusOperations( @@ -56,8 +56,8 @@ class CachedCurrenciesStatusesOperations( multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, + singleStakingBalanceSupplier = singleStakingBalanceSupplier, + multiStakingBalanceSupplier = multiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, ) { @@ -95,7 +95,7 @@ class CachedCurrenciesStatusesOperations( currencies = currencies, maybeNetworkStatuses = null, maybeQuotes = null, - maybeYieldBalances = null, + maybeStakingBalances = null, isUpdating = true, ) @@ -109,13 +109,13 @@ class CachedCurrenciesStatusesOperations( fun createCurrenciesStatuses( maybeQuotes: Either>, maybeNetworkStatuses: Either>, - maybeYieldBalances: Either>, + maybeStakingBalances: Either>, isUpdating: Boolean, ) = createCurrenciesStatuses( currencies = currencies, maybeQuotes = maybeQuotes, maybeNetworkStatuses = maybeNetworkStatuses, - maybeYieldBalances = maybeYieldBalances, + maybeStakingBalances = maybeStakingBalances, isUpdating = isUpdating, ) @@ -166,13 +166,13 @@ class CachedCurrenciesStatusesOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either>?, + maybeStakingBalances: Either>?, isUpdating: Boolean, ): Lce> = lce { isLoading.set(isUpdating) val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val yieldBalances = maybeYieldBalances?.bindEither() + val stakingBalances = maybeStakingBalances?.bindEither() val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { null } @@ -180,25 +180,25 @@ class CachedCurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) + val stakingBalance = findStakingBalanceOrNull(stakingBalances, currency, networkStatus) val currencyStatus = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.toOption(), maybeQuoteStatus = quote.toOption(), - maybeYieldBalance = yieldBalance.toOption(), + maybeStakingBalance = stakingBalance.toOption(), ) currencyStatus } } - private fun findYieldBalanceOrNull( - yieldBalances: List?, + private fun findStakingBalanceOrNull( + stakingBalances: List?, currency: CryptoCurrency, networkStatus: NetworkStatus?, - ): YieldBalance? { - if (yieldBalances.isNullOrEmpty()) return null + ): StakingBalance? { + if (stakingBalances.isNullOrEmpty()) return null val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value val address = networkStatus.getAddress() @@ -206,8 +206,8 @@ class CachedCurrenciesStatusesOperations( return if (supportedIntegration != null && address != null) { val stakingId = StakingID(integrationId = supportedIntegration, address = address) - yieldBalances.firstOrNull { it.stakingId == stakingId } - ?: YieldBalance.Error(stakingId = stakingId) + stakingBalances.firstOrNull { it.stakingId == stakingId } + ?: StakingBalance.Error(stakingId = stakingId) } else { null } @@ -302,9 +302,9 @@ class CachedCurrenciesStatusesOperations( private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, cryptoCurrencies: Map, - ): EitherFlow> { + ): EitherFlow> { return channelFlow { - val state = MutableStateFlow(emptyList()) + val state = MutableStateFlow(emptyList()) val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> stakingIdFactory.create( @@ -316,8 +316,11 @@ class CachedCurrenciesStatusesOperations( stakingIds.onEach { stakingId -> launch { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params( + userWalletId = userWalletId, + stakingId = stakingId, + ), ) .onEach { balance -> state.update { loadedBalances -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt index 3ba3d65d4e..3c2726eb06 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt @@ -8,12 +8,12 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.yield.supply.YieldSupplyStatus import java.math.BigDecimal /** - * Factory to create [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance]. + * Factory to create [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [StakingBalance]. * [REDACTED_AUTHOR] */ @@ -26,25 +26,25 @@ object CryptoCurrencyStatusFactory { get() = (this?.value as? QuoteStatus.Data)?.priceChange /** - * Creates [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance]. + * Creates [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [StakingBalance]. * * @param maybeNetworkStatus An optional network status containing blockchain information. * @param maybeQuoteStatus An optional quote status containing price information. - * @param maybeYieldBalance An optional yield balance containing staking information. + * @param maybeStakingBalance An optional staking balance containing staking information. */ fun create( currency: CryptoCurrency, maybeNetworkStatus: Option, maybeQuoteStatus: Option, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( currency = currency, value = createStatus( currency = currency, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, maybeQuoteStatus = maybeQuoteStatus, ), ) @@ -54,7 +54,7 @@ object CryptoCurrencyStatusFactory { currency: CryptoCurrency, maybeNetworkStatus: Option, maybeQuoteStatus: Option, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus.Value { val quoteStatus = maybeQuoteStatus.getOrNull() @@ -62,7 +62,12 @@ object CryptoCurrencyStatusFactory { is NetworkStatus.MissedDerivation -> createMissedDerivation(quoteStatus) is NetworkStatus.Unreachable -> createUnreachable(status, quoteStatus) is NetworkStatus.NoAccount -> createNoAccount(status, quoteStatus) - is NetworkStatus.Verified -> createStatus(currency, status, quoteStatus, maybeYieldBalance) + is NetworkStatus.Verified -> createStatus( + currency = currency, + status = status, + quoteStatus = quoteStatus, + maybeStakingBalance = maybeStakingBalance, + ) null -> CryptoCurrencyStatus.Loading } } @@ -106,7 +111,7 @@ object CryptoCurrencyStatusFactory { currency: CryptoCurrency, status: NetworkStatus.Verified, quoteStatus: QuoteStatus?, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { is NetworkStatus.Amount.Loaded -> amount.value @@ -118,7 +123,7 @@ object CryptoCurrencyStatusFactory { } } - val yieldBalance = maybeYieldBalance.getOrNull(id = currency.id, address = status.address) + val stakingBalance = maybeStakingBalance.getOrNull(id = currency.id, address = status.address) if (currency is CryptoCurrency.Token && currency.isCustom) { return createCustom( @@ -126,7 +131,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } @@ -138,7 +143,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } is QuoteStatus.Data -> { @@ -147,7 +152,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteValue, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } null -> CryptoCurrencyStatus.Loading @@ -166,7 +171,7 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus?, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.Custom { return CryptoCurrencyStatus.Custom( amount = amount, @@ -176,11 +181,11 @@ object CryptoCurrencyStatusFactory { hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) @@ -191,18 +196,18 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus?, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.NoQuote { return CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) @@ -213,7 +218,7 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus.Data, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.Loaded { return CryptoCurrencyStatus.Loaded( amount = amount, @@ -223,11 +228,11 @@ object CryptoCurrencyStatusFactory { hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus.source, ), ) @@ -243,20 +248,34 @@ object CryptoCurrencyStatusFactory { return yieldSupplyStatuses[id] } - private fun Option.getOrNull(id: CryptoCurrency.ID, address: NetworkAddress): YieldBalance.Data? { - val yieldBalance = this.getOrNull() as? YieldBalance.Data ?: return null + private fun Option.getOrNull( + id: CryptoCurrency.ID, + address: NetworkAddress, + ): StakingBalance.Data? { + return when (val stakingBalance = this.getOrNull()) { + is StakingBalance.Data.StakeKit -> { + val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value + val filteredTokenBalances = stakingBalance.balance.items.filter { + it.token.coinGeckoId == id.rawCurrencyId?.value + } - val isCurrentAddressStaking = yieldBalance.stakingId.address == address.defaultAddress.value - val filteredTokenBalances = yieldBalance.balance.items.filter { - it.token.coinGeckoId == id.rawCurrencyId?.value - } - - return if (isCurrentAddressStaking && filteredTokenBalances.isNotEmpty()) { - yieldBalance.copy( - balance = yieldBalance.balance.copy(items = filteredTokenBalances), - ) - } else { - null + if (isCurrentAddressStaking && filteredTokenBalances.isNotEmpty()) { + stakingBalance.copy( + balance = stakingBalance.balance.copy(items = filteredTokenBalances), + ) + } else { + null + } + } + is StakingBalance.Data.P2P -> { + // TODO p2p + val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value + if (isCurrentAddressStaking) stakingBalance else null + } + is StakingBalance.Empty, + is StakingBalance.Error, + null, + -> null } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 2679e1a047..a8f25caebe 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -16,6 +16,6 @@ class CurrenciesStatusesOperations { data class DataError(val cause: Throwable) : Error() - data object EmptyYieldBalances : Error() + data object EmptyStakingBalances : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt index 7a84e34972..14fb123230 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt @@ -6,7 +6,7 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance @@ -123,10 +123,10 @@ object TokenListFactory { } private fun CryptoCurrencyStatus.calculateBalance(): BigDecimal { - val yieldBalance = value.yieldBalance as? YieldBalance.Data - val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero() - val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero()) + val stakingBalance = value.stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero() + val totalFiatStakingBalance = totalStakingBalance.multiply(value.fiatRate.orZero()) - return value.fiatAmount?.plus(totalFiatYieldBalance).orZero() + return value.fiatAmount?.plus(totalFiatStakingBalance).orZero() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt index 47826a442d..db2f29b764 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt @@ -6,7 +6,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils @@ -153,17 +153,17 @@ object TotalFiatBalanceCalculator { } private fun CryptoCurrencyStatus.Loaded.getFiatStakingBalance(blockchainId: String): BigDecimal { - val yieldBalance = yieldBalance as? YieldBalance.Data - val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val stakingBalanceData = stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalanceData?.getTotalWithRewardsStakingBalance(blockchainId).orZero() - return fiatRate.times(stakingBalance) + return fiatRate.times(totalStakingBalance) } private fun CryptoCurrencyStatus.Custom.getFiatStakingBalance(blockchainId: String): BigDecimal { - val yieldBalance = yieldBalance as? YieldBalance.Data - val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val stakingBalanceData = stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalanceData?.getTotalWithRewardsStakingBalance(blockchainId).orZero() - return fiatRate?.times(stakingBalance).orZero() + return fiatRate?.times(totalStakingBalance).orZero() } private inline fun TotalFiatBalance.fold( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index b917edebde..59937cd45f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -11,8 +11,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -28,20 +28,20 @@ class CurrencyStatusProxyCreator { currency: CryptoCurrency, maybeQuoteStatus: Either, maybeNetworkStatus: Either, - maybeYieldBalance: Either?, + maybeStakingBalance: Either?, ): Either = either { val networkStatus = maybeNetworkStatus.bind() val quote = recover( block = { maybeQuoteStatus.bind() }, recover = { null }, ) - val yieldBalance = maybeYieldBalance?.getOrNull() + val stakingBalance = maybeStakingBalance?.getOrNull() createCurrencyStatus( currency = currency, quoteStatus = quote, networkStatus = networkStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } @@ -49,12 +49,12 @@ class CurrencyStatusProxyCreator { currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>, - maybeYieldBalances: Either>, + maybeStakingBalances: Either>, ): Either> = either { val networksStatuses = maybeNetworkStatuses.bind().toNonEmptySetOrNull() val quoteStatuses: Set? = maybeQuotes?.getOrNull()?.ifEmpty { null } - val yieldBalances = maybeYieldBalances.getOrNull() + val stakingBalances = maybeStakingBalances.getOrNull() currencies.map { currency -> val quote = quoteStatuses?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } @@ -63,11 +63,11 @@ class CurrencyStatusProxyCreator { val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value - val yieldBalance = if (supportedIntegration != null && address != null) { + val stakingBalance = if (supportedIntegration != null && address != null) { val stakingId = StakingID(integrationId = supportedIntegration, address = address) - yieldBalances?.firstOrNull { it.stakingId == stakingId } - ?: YieldBalance.Error(stakingId = stakingId) + stakingBalances?.firstOrNull { it.stakingId == stakingId } + ?: StakingBalance.Error(stakingId = stakingId) } else { null } @@ -76,7 +76,7 @@ class CurrencyStatusProxyCreator { currency = currency, quoteStatus = quote, networkStatus = networkStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } } @@ -85,13 +85,13 @@ class CurrencyStatusProxyCreator { currency: CryptoCurrency, quoteStatus: QuoteStatus?, networkStatus: NetworkStatus?, - yieldBalance: YieldBalance?, + stakingBalance: StakingBalance?, ): CryptoCurrencyStatus { return CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.toOption(), maybeQuoteStatus = quoteStatus.toOption(), - maybeYieldBalance = yieldBalance.toOption(), + maybeStakingBalance = stakingBalance.toOption(), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index bceb16c296..06b3f5f4ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -31,7 +31,7 @@ import timber.log.Timber * @property singleWalletBalanceFetcher balance fetcher of single-currency wallet * @property multiNetworkStatusFetcher networks statuses fetcher * @property multiQuoteStatusFetcher quotes statuses fetcher - * @property multiYieldBalanceFetcher yields balances fetcher + * @property multiStakingBalanceFetcher yields balances fetcher * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -44,7 +44,7 @@ class WalletBalanceFetcher internal constructor( private val singleWalletBalanceFetcher: BaseWalletBalanceFetcher, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -56,7 +56,7 @@ class WalletBalanceFetcher internal constructor( multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( @@ -71,7 +71,7 @@ class WalletBalanceFetcher internal constructor( singleWalletBalanceFetcher = SingleWalletBalanceFetcher(currenciesRepository = currenciesRepository), multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -164,8 +164,8 @@ class WalletBalanceFetcher internal constructor( val stakingIds = maybeStakingIds.mapNotNullTo(hashSetOf()) { it.getOrNull() } if (stakingIds.isNotEmpty()) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) .bind() } else { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index a9b9b31d66..1121756a55 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -154,7 +154,7 @@ internal object MockTokensStates { networkAddress = requireNotNull( value = MockNetworks.verifiedNetworksStatuses.first { it.network == status.currency.network }.value as? NetworkStatus.Verified, ).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ) @@ -166,7 +166,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ) @@ -185,7 +185,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt index 22c3bb6eff..84fa8cedeb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt @@ -14,8 +14,8 @@ import com.tangem.domain.models.network.NetworkStatus.Amount import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.domain.models.yield.supply.YieldSupplyStatus import com.tangem.domain.staking.model.StakingIntegrationID @@ -54,7 +54,7 @@ class CryptoCurrencyStatusFactoryTest { inner class MissedDerivation { private val networkStatus = NetworkStatus.MissedDerivation.toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is MissedDerivation and QuoteStatus is Data`() { @@ -63,7 +63,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -82,7 +82,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -97,7 +97,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -112,7 +112,7 @@ class CryptoCurrencyStatusFactoryTest { private val networkStatus = NetworkStatus.Unreachable(address = networkAddress).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is Unreachable and QuoteStatus is Data`() { @@ -121,7 +121,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -141,7 +141,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -161,7 +161,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -185,7 +185,7 @@ class CryptoCurrencyStatusFactoryTest { source = StatusSource.ACTUAL, ).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is NoAccount and QuoteStatus is Data`() { @@ -194,7 +194,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -217,7 +217,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -239,7 +239,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -261,7 +261,7 @@ class CryptoCurrencyStatusFactoryTest { private val networkStatus = createVerified(amounts = mapOf(currency.id to Amount.NotFound)).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is Verified with Amount is NotFound and QuoteStatus is Data`() { @@ -270,7 +270,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -289,7 +289,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -304,7 +304,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -317,7 +317,7 @@ class CryptoCurrencyStatusFactoryTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Loading { - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is null`() { @@ -326,7 +326,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = none(), maybeQuoteStatus = none(), // not relevant for this test, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -344,7 +344,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), // not relevant for this test, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -364,7 +364,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -387,14 +387,14 @@ class CryptoCurrencyStatusFactoryTest { fun `network is Verified, QuoteStatus is null, YieldBalance is null`() { // Arrange val maybeQuoteStatus = none() - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -405,7 +405,7 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = null, fiatRate = null, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -420,7 +420,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified, QuoteStatus is Data, YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -444,7 +444,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert @@ -455,9 +455,9 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = null, @@ -490,20 +490,20 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is null`() { // Arrange - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert val expected = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.TEN, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!, hasCurrentNetworkTransactions = true, pendingTransactions = pendingTransactions[currency.id]!!, @@ -517,7 +517,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -541,15 +541,15 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert val expected = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.TEN, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!, @@ -576,14 +576,14 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is null`() { // Arrange - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -592,7 +592,7 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -606,7 +606,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -630,7 +630,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert @@ -639,9 +639,9 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = null, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt index 7dbbeec80b..93087cadfc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt @@ -137,7 +137,7 @@ class PriceChangeCalculatorTest { fiatAmount = amount, fiatRate = BigDecimal.ONE, priceChange = priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt index 5c2a253292..7f08ae5dbc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.YieldBalanceItem import io.mockk.every import io.mockk.mockk @@ -192,7 +192,7 @@ class TotalFiatBalanceCalculatorTest { createCustom( currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(20), // It is important to use `BalanceType.REWARDS` because Cardano should not include the full // staking balance. See `getTotalWithRewardsStakingBalance`. @@ -207,7 +207,7 @@ class TotalFiatBalanceCalculatorTest { createCustom( currency = cryptoCurrencyFactory.stellar, fiatAmount = BigDecimal.ONE, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(9), balanceType = BalanceType.STAKED, ), @@ -246,7 +246,7 @@ class TotalFiatBalanceCalculatorTest { createLoaded( currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(20), // It is important to use `BalanceType.REWARDS` because Cardano should not include the full // staking balance. See `getTotalWithRewardsStakingBalance`. @@ -261,7 +261,7 @@ class TotalFiatBalanceCalculatorTest { createLoaded( currency = cryptoCurrencyFactory.stellar, fiatAmount = BigDecimal.ONE, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(9), balanceType = BalanceType.STAKED, ), @@ -497,7 +497,7 @@ class TotalFiatBalanceCalculatorTest { currency = currency, value = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.ONE, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -545,7 +545,7 @@ class TotalFiatBalanceCalculatorTest { private fun createCustom( currency: CryptoCurrency, fiatAmount: BigDecimal?, - yieldBalance: YieldBalance? = null, + stakingBalance: StakingBalance.Data.StakeKit? = null, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( currency = currency, @@ -554,7 +554,7 @@ class TotalFiatBalanceCalculatorTest { fiatAmount = fiatAmount, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -567,7 +567,7 @@ class TotalFiatBalanceCalculatorTest { private fun createLoaded( currency: CryptoCurrency, fiatAmount: BigDecimal, - yieldBalance: YieldBalance? = null, + stakingBalance: StakingBalance.Data.StakeKit? = null, source: StatusSource = StatusSource.ACTUAL, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( @@ -577,7 +577,7 @@ class TotalFiatBalanceCalculatorTest { fiatAmount = fiatAmount, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -587,8 +587,8 @@ class TotalFiatBalanceCalculatorTest { ) } - private fun createYieldBalance(amount: BigDecimal, balanceType: BalanceType): YieldBalance.Data { - return YieldBalance.Data( + private fun createStakeKitBalance(amount: BigDecimal, balanceType: BalanceType): StakingBalance.Data.StakeKit { + return StakingBalance.Data.StakeKit( stakingId = mockk(), source = StatusSource.ACTUAL, balance = YieldBalanceItem( diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index f2eb68dbdd..2af6690bc8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -12,7 +12,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.wallet.FetchingSource.* import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher @@ -41,7 +41,7 @@ internal class WalletBalanceFetcherTest { private val singleWalletBalanceFetcher: SingleWalletBalanceFetcher = mockk() private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( @@ -51,7 +51,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher = singleWalletBalanceFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -65,7 +65,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher, multiNetworkStatusFetcher, multiQuoteStatusFetcher, - multiYieldBalanceFetcher, + multiStakingBalanceFetcher, ) } @@ -91,7 +91,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -122,7 +122,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -156,7 +156,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -188,7 +188,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -234,7 +234,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -280,7 +280,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -292,7 +292,7 @@ internal class WalletBalanceFetcherTest { } val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -308,7 +308,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -326,7 +326,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -372,7 +372,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -414,7 +414,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -462,7 +462,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -485,7 +485,7 @@ internal class WalletBalanceFetcherTest { appCurrencyId = null, ) - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -503,7 +503,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -525,7 +525,7 @@ internal class WalletBalanceFetcherTest { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -553,7 +553,7 @@ internal class WalletBalanceFetcherTest { appCurrencyId = null, ) - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -569,7 +569,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns Unit.right() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -586,7 +586,7 @@ internal class WalletBalanceFetcherTest { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -642,7 +642,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -692,7 +692,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt index 715c90c6a3..5bb7f32a1a 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt @@ -51,7 +51,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = BigDecimal.ZERO, fiatRate = tokenFiatRate, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -103,7 +103,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = null, fiatRate = null, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -131,7 +131,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = BigDecimal.ZERO, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index 254c8c6d4a..3636c6f6ec 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -59,7 +59,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -90,7 +90,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -135,7 +135,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt index 7cfd18cb02..efcc805d31 100644 --- a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt +++ b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt @@ -168,7 +168,7 @@ class FeeCalculationUtilsTest { fiatAmount = BigDecimal.ZERO, fiatRate = BigDecimal.ZERO, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), networkAddress = mockk(relaxed = true), diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml index 2c5de23a8e..79c9fb9f11 100644 --- a/features/staking/impl/detekt-baseline-debug.xml +++ b/features/staking/impl/detekt-baseline-debug.xml @@ -5,7 +5,6 @@ BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean BooleanPropertyNaming:StakingModel.kt$StakingModel$val noBalanceState = balanceState == null - BooleanPropertyNaming:StakingModel.kt$StakingModel$val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean CanBeNonNullable:StakingScreen.kt$bottomSheetConfig: TangemBottomSheetConfig? @@ -42,7 +41,6 @@ UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) } UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) } UnsafeCallOnNullableType:StakingModel.kt$StakingModel$tonAccountInitializeTransaction!! - UseOrEmpty:StakingModel.kt$StakingModel$yieldBalance?.balance?.items ?: emptyList() UseOrEmpty:StakingTransactionSender.kt$StakingTransactionSender$getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, ).getOrNull() ?: "" VarCouldBeVal:StakingModel.kt$StakingModel$private var actionsJobHolder: JobHolder = JobHolder() VarCouldBeVal:StakingModel.kt$StakingModel$private var approvalJobHolder: JobHolder = JobHolder() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index eebecedbcf..e4b046c12c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -190,9 +190,9 @@ internal class StakingModel @Inject constructor( private val balancesToShow: List get() { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit return invalidatePendingTransactionsUseCase( - balanceItems = yieldBalance?.balance?.items ?: emptyList(), + balanceItems = stakeKitBalance?.balance?.items.orEmpty(), stakingActions = stakingActions, token = yield.token, ).getOrElse { emptyList() } @@ -277,10 +277,10 @@ internal class StakingModel @Inject constructor( modelScope.launch { val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo val noBalanceState = balanceState == null - val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit when { - isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { + isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> { stakingEventFactory.createStakingValidatorsUnavailableAlert() return@launch } @@ -1116,7 +1116,7 @@ internal class StakingModel @Inject constructor( private suspend fun onDataLoaded(status: CryptoCurrencyStatus) { if (!isInitialInfoAnalyticSent) { isInitialInfoAnalyticSent = true - val balances = status.value.yieldBalance as? YieldBalance.Data + val balances = status.value.stakingBalance as? StakingBalance.Data.StakeKit paramsInterceptorHolder.addParamsInterceptor( interceptor = StakingParamsInterceptor(status.currency.symbol), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 5d548f7ab4..20230aecef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -10,7 +10,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType.Companion.isClickable -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -77,11 +77,11 @@ internal class BalanceItemConverter( val isIncludeStakingTotalBalance = BlockchainUtils.isIncludeStakingTotalBalance( blockchainId = cryptoCurrencyStatus.currency.network.rawId, ) - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit return if (isIncludeStakingTotalBalance) { amount } else { - amount - yieldBalance?.getRewardStakingBalance().orZero() + amount - stakeKitBalance?.getRewardStakingBalance().orZero() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 4c47f26565..68e09c30c5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -9,7 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -24,9 +24,9 @@ internal class RewardsValidatorStateConverter( private val yield: Yield, ) : Converter { override fun convert(value: Unit): StakingStates.RewardsValidatorsState { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return if (yieldBalance is YieldBalance.Data) { - val balances = yieldBalance.balance.items + val stakingBalance = cryptoCurrencyStatus.value.stakingBalance + return if (stakingBalance is StakingBalance.Data.StakeKit) { + val balances = stakingBalance.balance.items StakingStates.RewardsValidatorsState.Data( isPrimaryButtonEnabled = true, rewards = balances @@ -35,6 +35,7 @@ internal class RewardsValidatorStateConverter( .toPersistentList(), ) } else { + // TODO p2p StakingStates.RewardsValidatorsState.Empty() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 75c557eb38..8c118c5572 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -36,11 +36,11 @@ internal class YieldBalancesConverter( val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit val balanceToShowItems = balancesToShowProvider() - return if (yieldBalance != null || balanceToShowItems.any { it.isPending }) { - val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance() + return if (stakeKitBalance != null || balanceToShowItems.any { it.isPending }) { + val cryptoRewardsValue = stakeKitBalance?.getRewardStakingBalance() val fiatRate = cryptoCurrencyStatus.value.fiatRate val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) { @@ -49,13 +49,13 @@ internal class YieldBalancesConverter( null } val type = getRewardBlockType() - val pendingRewardsConstraints = yieldBalance?.balance?.items + val pendingRewardsConstraints = stakeKitBalance?.balance?.items ?.firstOrNull { it.type == BalanceType.REWARDS } ?.pendingActionsConstraints ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } InnerYieldBalanceState.Data( - integrationId = yieldBalance?.stakingId?.integrationId, + integrationId = stakeKitBalance?.stakingId?.integrationId, reward = YieldReward( rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = fiatRewardsValue.format { @@ -71,6 +71,7 @@ internal class YieldBalancesConverter( balances = balanceToShowItems.mapBalances(), ) } else { + // TODO p2p InnerYieldBalanceState.Empty } } @@ -84,8 +85,8 @@ internal class YieldBalancesConverter( private fun getRewardBlockType(): RewardBlockType { val blockchainId = cryptoCurrencyStatus.currency.network.rawId - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val rewards = yieldBalance?.balance?.items + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit + val rewards = stakeKitBalance?.balance?.items ?.filter { it.type == BalanceType.REWARDS && !it.amount.isZero() } val isActionable = rewards?.any { it.pendingActions.isNotEmpty() } == true diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 73b8c296f2..30f437ca80 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -35,7 +35,7 @@ internal class SetConfirmationStateAssentTransformer( isFeeApproximate = isFeeApproximate, ), isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 6f745765ff..8b650155fb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -26,7 +26,7 @@ internal class SetConfirmationStateCompletedTransformer( return if (this is StakingStates.ConfirmationState.Data) { copy( isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, innerState = InnerConfirmationStakingState.COMPLETED, footerText = TextReference.EMPTY, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index db0da4876d..16ff686c06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -15,7 +15,7 @@ internal class SetConfirmationStateResetAssentTransformer( confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) { confirmationState.copy( isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, innerState = InnerConfirmationStakingState.ASSENT, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e9e9fbe02b..2f55f146f8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -90,7 +90,7 @@ internal class SetInitialDataStateTransformer( val status = cryptoCurrencyStatus.value return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = with(status) { - !amount.isNullOrZero() && sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + !amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, infoItems = getInfoItems(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 235472ef08..da0c0d9a9a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -37,7 +37,7 @@ internal class SetConfirmationStateAssentApprovalTransformer( isFeeApproximate = false, ), isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, isApprovalNeeded = true, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 66d3c56eac..3194f4e9ae 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -127,7 +127,7 @@ internal class AddStakingNotificationsTransformer( } val isActualSources = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() } return prevState.copy( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 8959e90124..bdda0a7ac5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -134,7 +134,7 @@ internal class StakingInfoNotificationsFactory( private fun MutableList.addTronRevoteNotification() { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId) - val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance + val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance ?.items?.any { it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index bdedae6271..f45527c13b 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -36,7 +36,6 @@ NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingCryptoAmount NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingEntryInfo - NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$yieldBalance PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$network PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$token PropertyUsedBeforeDeclaration:TokenDetailsModel.kt$TokenDetailsModel$uiState @@ -48,7 +47,6 @@ UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent$mapOf() UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent.Notice$mapOf() UseOrEmpty:ExchangeStatusFactory.kt$ExchangeStatusFactory$savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.toSet() ?.getQuotesOrEmpty() ?: emptySet() - UseOrEmpty:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$yieldBalance?.balance?.items ?: emptyList() VarCouldBeVal:TokenDetailsModel.kt$TokenDetailsModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 95139be280..0f9a0741ac 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance @@ -29,8 +29,8 @@ internal class TokenDetailsBalanceSelectStateConverter( return this } - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance( + val stakingBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data + val stakingCryptoAmount = stakingBalance?.getTotalWithRewardsStakingBalance( cryptoCurrencyStatus.currency.network.rawId, ) val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 3243636297..976f4f2e87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -73,7 +73,7 @@ internal class TokenDetailsLoadedBalanceConverter( status: CryptoCurrencyStatus, ): TokenDetailsBalanceBlockState { val stakingCryptoAmount = - (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance( + (status.value.stakingBalance as? StakingBalance.Data)?.getTotalWithRewardsStakingBalance( status.currency.network.rawId, ) val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index b16d554602..e87c73a48e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.RewardBlockType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -54,18 +54,29 @@ internal class TokenDetailsStakingInfoConverter( } } + @Suppress("CyclomaticComplexMethod") private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { - val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + val stakingBalance = status.value.stakingBalance as? StakingBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance(status.currency.network.rawId) - val pendingBalances = yieldBalance?.balance?.items ?: emptyList() + val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId) + + val hasPendingBalances = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.isNotEmpty() + is StakingBalance.Data.P2P -> !stakingBalance.unstakingAmount.isNullOrZero() + null -> false + } + val pendingAmount = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.sumOf { it.amount } + is StakingBalance.Data.P2P -> stakingBalance.unstakingAmount + null -> BigDecimal.ZERO + } val iconState = state.tokenInfoBlockState.iconState Timber.i( """ getStakingInfoBlock: - – yieldBalance: $yieldBalance + – stakingBalance: ${stakingBalance ?: "null"} – stakingCryptoAmount: $stakingCryptoAmount – stakingEntryInfo: $stakingEntryInfo """.trimIndent(), @@ -73,16 +84,24 @@ internal class TokenDetailsStakingInfoConverter( return when { stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { - if (pendingBalances.isEmpty()) { + if (!hasPendingBalances) { getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) } else { - getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) + getStakedBlockWithFiatAmount(status, pendingAmount, null) } } stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { null } - else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) + else -> getStakedBlockWithFiatAmount( + status = status, + stakingAmount = stakingCryptoAmount, + rewardAmount = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.getRewardStakingBalance() + is StakingBalance.Data.P2P -> stakingBalance.totalRewards + else -> BigDecimal.ZERO + }, + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 8c1278e5b6..83ffca53f6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -8,7 +8,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -239,7 +239,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( token = currencyStatus.currency.symbol, blockchain = currencyStatus.currency.network.name, action = "Staking", - state = if (currencyStatus.value.yieldBalance is YieldBalance.Data) { + state = if (currencyStatus.value.stakingBalance is StakingBalance.Data) { "Enabled" } else { "Disabled" 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 197cb45b57..23a5a6ea50 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 @@ -31,7 +31,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked @@ -128,7 +128,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val urlOpener: UrlOpener, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -529,8 +529,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params( + userWalletId = userWalletId, + stakingIds = stakingIds, + ), ) .onLeft { Timber.e("Unable to fetch yield balances: $it") } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 21f42375ef..4955e962ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -63,12 +63,12 @@ internal class CryptoCurrencyToDraggableItemConverter( } private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) + val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) ?.multiply(fiatRate).orZero() val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } + return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt index 8a8dc8252b..2851d268b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -56,12 +56,12 @@ internal class CryptoCurrencyToDraggableItemConverterV2( } private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) + val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) ?.multiply(fiatRate).orZero() val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } + return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 0c3ac24ecd..7ea7a4d42a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -137,7 +137,7 @@ internal class SetVisaInfoTransformer( fiatAmount = visaCurrency.balances.available.multiply(visaCurrency.fiatRate), fiatRate = visaCurrency.fiatRate, priceChange = visaCurrency.priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), From 7a1b1ff5a3110abf54942d66e3bd4c853a1b0a7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 10:46:19 +0200 Subject: [PATCH 036/130] Updated on 2026-08-14 --- .../auth/DefaultP2PEthPoolAuthProvider.kt | 3 ++- .../api/common/config/P2PEthPool.kt | 8 ++----- .../datasource/api/ethpool/P2PEthPoolApi.kt | 4 +--- .../managers/ProdApiConfigsManagerTest.kt | 11 +++++++-- .../staking/DefaultP2PEthPoolRepository.kt | 24 ++++--------------- .../DefaultMultiStakingBalanceFetcher.kt | 4 ++-- .../tangem/domain/models/staking/StakingID.kt | 5 ---- .../domain/staking/model/StakingOption.kt | 6 ++--- .../model/ethpool/P2PEthPoolNetwork.kt | 10 ++++++-- .../staking/model/ethpool/P2PStakingConfig.kt | 14 +++++++++++ .../staking/model/StakingIntegrationID.kt | 7 ++++-- .../repositories/P2PEthPoolRepository.kt | 12 +++++++--- .../domain/staking/StakingIdFactoryTest.kt | 2 +- .../staking/StakingIntegrationIDTest.kt | 2 +- gradle/tangem_dependencies.toml | 2 +- 15 files changed, 62 insertions(+), 52 deletions(-) create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt index 5839903010..5f54236082 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -1,6 +1,7 @@ package com.tangem.tap.network.auth import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider internal class DefaultP2PEthPoolAuthProvider( @@ -11,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider( val keys = environmentConfigStorage.getConfigSync().p2pApiKey ?: error("No P2P api keys provided") - return keys.mainnet + return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt index b699172b24..aa1b28c629 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.utils.ProviderSuspend @@ -22,12 +23,7 @@ internal class P2PEthPool( private fun getInitialEnvironment(): ApiEnvironment { return when (BuildConfig.BUILD_TYPE) { MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK - DEBUG_BUILD_TYPE, - INTERNAL_BUILD_TYPE, - EXTERNAL_BUILD_TYPE, - RELEASE_BUILD_TYPE, - -> ApiEnvironment.PROD - else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt index 60503742bb..cf3ab2f3e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt @@ -23,9 +23,7 @@ interface P2PEthPoolApi { * @param network Ethereum pool network: "mainnet" or "hoodi" (testnet) */ @GET("api/v1/staking/pool/{network}/vaults") - suspend fun getVaults( - @Path("network") network: String = "mainnet", - ): ApiResponse> + suspend fun getVaults(@Path("network") network: String): ApiResponse> /** * Prepare deposit transaction diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 37f48a1f83..bf6b755bc5 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -299,11 +300,17 @@ internal class ProdApiConfigsManagerTest { } private fun createP2PModel(): TestModel { + val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) { + ApiEnvironment.DEV to "https://api-test.p2p.org/" + } else { + ApiEnvironment.PROD to "https://api.p2p.org/" + } + return TestModel( id = ApiConfig.ID.P2PEthPool, expected = ApiEnvironmentConfig( - environment = ApiEnvironment.PROD, - baseUrl = "https://api.p2p.org/", + environment = environment, + baseUrl = baseUrl, headers = mapOf( "Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" }, "accept" to ProviderSuspend { "application/json" }, diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index b97325f373..85f3038652 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -223,30 +223,18 @@ internal class DefaultP2PEthPoolRepository( .map { vaults -> if (vaults.isEmpty()) { return@map StakingAvailability.TemporaryUnavailable - } - - val vault = findPublicVault(vaults = vaults) - - if (vault != null) { - StakingAvailability.Available(StakingOption.P2P(vault)) } else { - StakingAvailability.TemporaryUnavailable + StakingAvailability.Available(StakingOption.P2P(vaults)) } } } override suspend fun getStakingAvailabilitySync(): StakingAvailability { val vaults = getVaultsSync() - if (vaults.isEmpty()) { - return StakingAvailability.TemporaryUnavailable - } - - val vault = findPublicVault(vaults = vaults) - - return if (vault != null) { - StakingAvailability.Available(StakingOption.P2P(vault)) - } else { + return if (vaults.isEmpty()) { StakingAvailability.TemporaryUnavailable + } else { + StakingAvailability.Available(StakingOption.P2P(vaults)) } } @@ -257,8 +245,4 @@ internal class DefaultP2PEthPoolRepository( private fun getVaultsFlow(): Flow> { return p2pEthPoolVaultsStore.get() } - - private fun findPublicVault(vaults: List): P2PEthPoolVault? { - return vaults.firstOrNull { vault -> !vault.isPrivate } - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index 99395f26b0..8a2411fc11 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -24,7 +24,7 @@ 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.model.ethpool.P2PStakingConfig import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching @@ -155,7 +155,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( for (address in addresses) { runSuspendCatching { val response = p2pApi.getAccountInfo( - network = P2PEthPoolNetwork.MAINNET.value, + network = P2PStakingConfig.activeNetwork.value, delegatorAddress = address, vaultAddress = vault.vaultAddress, ) 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 e684f70531..64c8249582 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,11 +2,6 @@ package com.tangem.domain.models.staking import kotlinx.serialization.Serializable -enum class StakingProvider { - STAKEKIT, - P2P, -} - @Serializable data class StakingID( val integrationId: String, 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 77078dd75e..60f9674e3f 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 @@ -39,11 +39,11 @@ sealed interface StakingOption { * P2P pooled staking option * Wraps P2P ETH Pool vault information */ - data class P2P(val vault: P2PEthPoolVault) : StakingOption { + data class P2P(val vaults: List) : StakingOption { override val integrationId: String = "p2p-ethereum-pooled" - override val apy: SerializedBigDecimal = vault.apy + override val apy: SerializedBigDecimal = vaults.maxOf { it.apy } override val token: YieldToken = createEthToken() - override val isAvailable: Boolean = !vault.isPrivate + override val isAvailable: Boolean = vaults.isNotEmpty() private fun createEthToken(): YieldToken { // TODO return YieldToken( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt index 2321e39114..dfbed790f9 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt @@ -11,6 +11,8 @@ enum class P2PEthPoolNetwork( val value: String, val displayName: String, val chainId: Int, + val stakingNetworkId: String, + val isTestnet: Boolean, ) { /** * Ethereum mainnet @@ -20,16 +22,20 @@ enum class P2PEthPoolNetwork( value = "mainnet", displayName = "Ethereum", chainId = 1, + stakingNetworkId = "ethereum", + isTestnet = false, ), /** - * Ethereum testnet (Holesky) + * Ethereum testnet (Hoodi) * Chain ID: 17000 */ TESTNET( value = "hoodi", - displayName = "Holesky Testnet", + displayName = "Hoodi Testnet", chainId = 17000, + stakingNetworkId = "ethereum/test", + isTestnet = true, ), ; diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt new file mode 100644 index 0000000000..52c0f311e8 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.staking.model.ethpool + +/** + * Configuration for P2P Ethereum staking network. + * + * Change [USE_TESTNET] to switch between testnet and mainnet. + */ +object P2PStakingConfig { + + const val USE_TESTNET: Boolean = true + + val activeNetwork: P2PEthPoolNetwork + get() = if (USE_TESTNET) P2PEthPoolNetwork.TESTNET else P2PEthPoolNetwork.MAINNET +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt index 3ce629bf6f..1bf895cbcc 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt @@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toMigratedCoinId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig /** * Represents a staking integration identifier. @@ -101,8 +102,10 @@ sealed interface StakingIntegrationID { enum class P2P : StakingIntegrationID { EthereumPooled { override val value: String = "p2p-ethereum-pooled" - override val blockchain: Blockchain = Blockchain.Ethereum - override val networkId: String = "ethereum" + override val blockchain: Blockchain + get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum + override val networkId: String + get() = P2PStakingConfig.activeNetwork.stakingNetworkId }, } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt index fabb8176d1..8a116ff9b6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt @@ -2,7 +2,13 @@ package com.tangem.domain.staking.repositories import arrow.core.Either import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.ethpool.* +import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount +import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult +import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork +import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward +import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.domain.staking.model.stakekit.StakingError import kotlinx.coroutines.flow.Flow @@ -13,7 +19,7 @@ interface P2PEthPoolRepository { * * @param network P2P network (MAINNET or TESTNET) */ - suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET) + suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork) /** * Get list of available staking vaults @@ -22,7 +28,7 @@ interface P2PEthPoolRepository { * @return Either error or list of vaults with APY, capacity, fees */ suspend fun getVaults( - network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET, + network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork, ): Either> /** diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index dc18df883b..996ea8cca0 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -149,7 +149,7 @@ internal class StakingIdFactoryTest { expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano), ), CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain), expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled), ), CreateModel( diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt index ed60c5fd0c..745af46793 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt @@ -151,7 +151,7 @@ class StakingIntegrationIDTest { expected = StakingIntegrationID.StakeKit.Coin.Cardano, ), CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain), expected = StakingIntegrationID.P2P.EthereumPooled, ), CreateModel( diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 487e084342..8c10bcaf9d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1309" +tangemBlockchainSdk = "develop-1317" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-568" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8568277f95d315bf64d59833748a27f3e8c61ed1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 14:05:09 +0300 Subject: [PATCH 037/130] Updated on 2026-08-14 --- .../appcurrency/AppCurrencyResponseStore.kt | 3 +++ .../DefaultAppCurrencyResponseStore.kt | 8 ++++++ .../DefaultAppCurrencyRepository.kt | 21 ++++++---------- .../appcurrency/di/AppCurrencyDataModule.kt | 6 ++--- .../data/onramp/DefaultHotCryptoRepository.kt | 25 ++++++++----------- .../tangem/data/onramp/di/OnrampDataModule.kt | 5 ++-- 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt index 5f9e2c12a9..45e05a8df4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt @@ -15,4 +15,7 @@ interface AppCurrencyResponseStore { /** Get [CurrenciesResponse.Currency] synchronously or null */ suspend fun getSyncOrNull(): CurrenciesResponse.Currency? + + /** Store [CurrenciesResponse.Currency] */ + suspend fun store(currency: CurrenciesResponse.Currency) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt index 1c268c06c7..9225bd5ab8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import kotlinx.coroutines.flow.Flow /** @@ -25,4 +26,11 @@ internal class DefaultAppCurrencyResponseStore( PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) } + + override suspend fun store(currency: CurrenciesResponse.Currency) { + appPreferencesStore.storeObject( + PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + currency, + ) + } } \ No newline at end of file diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index bcc535bed2..aae64594c5 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -5,12 +5,8 @@ import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -24,7 +20,7 @@ import org.joda.time.Duration internal class DefaultAppCurrencyRepository( private val tangemTechApi: TangemTechApi, - private val appPreferencesStore: AppPreferencesStore, + private val appCurrencyResponseStore: AppCurrencyResponseStore, private val availableAppCurrenciesStore: AvailableAppCurrenciesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, @@ -35,15 +31,15 @@ internal class DefaultAppCurrencyRepository( override fun getSelectedAppCurrency(): Flow { return channelFlow { launch { - appPreferencesStore - .getObject(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + appCurrencyResponseStore + .get() .filterNotNull() .map(appCurrencyConverter::convert) .collect(::send) } withContext(dispatchers.io) { - if (appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) == null) { + if (appCurrencyResponseStore.getSyncOrNull() == null) { fetchDefaultAppCurrency() } } @@ -70,17 +66,14 @@ internal class DefaultAppCurrencyRepository( "Unable to find app currency with provided code: $currencyCode" } - appPreferencesStore.storeObject( - key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, - value = currency, - ) + appCurrencyResponseStore.store(currency) } } override suspend fun fetchDefaultAppCurrency(isRefresh: Boolean) { withContext(dispatchers.io) { fetchAvailableCurrenciesIfExpired(isRefresh) - val appCurrency = appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + val appCurrency = appCurrencyResponseStore.getSyncOrNull()?.code changeAppCurrency(appCurrency ?: DEFAULT_CURRENCY_CODE) } } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt index d9cb9be5f7..719823ed4a 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt @@ -3,8 +3,8 @@ package com.tangem.data.appcurrency.di import com.tangem.data.appcurrency.DefaultAppCurrencyRepository import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -21,17 +21,17 @@ internal object AppCurrencyDataModule { @Singleton fun provideAppCurrencyRepository( tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + appCurrencyResponseStore: AppCurrencyResponseStore, availableAppCurrenciesStore: AvailableAppCurrenciesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): AppCurrencyRepository { return DefaultAppCurrencyRepository( tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferencesStore, availableAppCurrenciesStore = availableAppCurrenciesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, + appCurrencyResponseStore = appCurrencyResponseStore, ) } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index cfffffb159..b1552db7cc 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -11,13 +11,10 @@ import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain @@ -39,13 +36,13 @@ import timber.log.Timber /** * Default implementation of [HotCryptoRepository] * - * @property excludedBlockchains excluded blockchains - * @property hotCryptoResponseStore store of `HotCryptoResponse` - * @property userWalletsStore store of `UserWallet` - * @property tangemTechApi tangem tech api - * @property appPreferencesStore app preferences store - * @property dispatchers dispatchers - * @property analyticsEventHandler analytics event handler + * @property excludedBlockchains excluded blockchains + * @property hotCryptoResponseStore store of `HotCryptoResponse` + * @property userWalletsStore store of `UserWallet` + * @property tangemTechApi tangem tech api + * @property appCurrencyResponseStore store of current app currency + * @property dispatchers dispatchers + * @property analyticsEventHandler analytics event handler * [REDACTED_AUTHOR] */ @@ -56,7 +53,7 @@ internal class DefaultHotCryptoRepository( private val hotCryptoResponseStore: HotCryptoResponseStore, private val userWalletsStore: UserWalletsStore, private val tangemTechApi: TangemTechApi, - private val appPreferencesStore: AppPreferencesStore, + private val appCurrencyResponseStore: AppCurrencyResponseStore, private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -112,8 +109,8 @@ internal class DefaultHotCryptoRepository( } private fun getHotCryptoFlow(): Flow { - return appPreferencesStore - .getObject(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + return appCurrencyResponseStore + .get() .map { it?.id ?: "usd" } .distinctUntilChanged() .map { getHotCrypto(appCurrencyId = it).getOrNull() } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index b953fd69c7..2e80138833 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore @@ -104,7 +105,7 @@ internal object OnrampDataModule { hotCryptoResponseStore: HotCryptoResponseStore, userWalletsStore: UserWalletsStore, tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, userTokensResponseStore: UserTokensResponseStore, @@ -114,7 +115,7 @@ internal object OnrampDataModule { hotCryptoResponseStore = hotCryptoResponseStore, userWalletsStore = userWalletsStore, tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferencesStore, + appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, userTokensResponseStore = userTokensResponseStore, From ca2804b0e01ae2e9179778e3b8043848aaa6fb6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 15:09:07 +0400 Subject: [PATCH 038/130] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 12 +++ .../AssociateAppWithWalletsErrorResponse.kt | 9 ++ .../AssociateApplicationIdWithWalletsBody.kt | 9 ++ .../data/common/currency/UserTokensSaver.kt | 66 ++++++++++++--- .../common/currency/UserTokensSaverTest.kt | 11 +-- data/wallets/build.gradle.kts | 15 ++-- .../data/wallets/DefaultWalletsRepository.kt | 82 ++++++++++++++----- .../data/wallets/di/WalletsDataModule.kt | 7 ++ .../wallets/DefaultWalletsRepositoryTest.kt | 9 ++ 9 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index d3e09a5f52..aec86d12d2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -50,6 +50,12 @@ interface TangemTechApi { @Body userTokens: UserTokensResponse, ): ApiResponse + @PUT("/v1/wallets/{walletId}/tokens") + suspend fun saveTokens( + @Path(value = "walletId") userId: String, + @Body userTokens: UserTokensResponse, + ): ApiResponse + /** Returns referral status by [walletId] */ @GET("v1/referral/{walletId}") suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse @@ -129,6 +135,12 @@ interface TangemTechApi { @Body body: List, ): ApiResponse + @PUT("/v1/user-wallets/applications/{application_id}/wallets") + suspend fun associateApplicationIdWithWalletsV2( + @Path("application_id") applicationId: String, + @Body body: AssociateApplicationIdWithWalletsBody, + ): ApiResponse + @GET("v1/user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt new file mode 100644 index 0000000000..14696568f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssociateAppWithWalletsErrorResponse( + @Json(name = "missingWalletIds") val missingWalletIds: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt new file mode 100644 index 0000000000..59dfa30b85 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssociateApplicationIdWithWalletsBody( + @Json(name = "walletIds") val walletIds: List, +) \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 61f43d9ad7..2b4e65a095 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -2,12 +2,17 @@ package com.tangem.data.common.currency import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.tokens.UserTokensBackwardCompatibility +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.retryer.Retryer @@ -50,24 +55,26 @@ class UserTokensSaver( response: UserTokensResponse, useEnricher: Boolean = true, onFailSend: () -> Unit = {}, - ) { - withContext(dispatchers.default) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + ) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + if (userWallet == null) { + Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") + onFailSend() + return@withContext + } + + if (accountsFeatureToggles.isFeatureEnabled) { + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + + pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) + } else { val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet?.name, + walletName = userWallet.name, walletType = WalletType.from(userWallet), ) - safeApiCall( - call = { - withContext(dispatchers.io) { - tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse) - .bind() - } - }, - onError = { onFailSend() }, - ) + pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) } } @@ -88,6 +95,39 @@ class UserTokensSaver( ) } + private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() }, + onError = { onFailSend() }, + ) + } + + private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { + val apiResponse = tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ) + + val isWalletNotFound = apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND) + + if (isWalletNotFound) { + tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind() + + tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ).bind() + } else { + apiResponse.bind() + } + }, + onError = { onFailSend() }, + ) + } + private fun UserTokensResponse.applyCompatibility(): UserTokensResponse { return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 24c2d207e7..bc51068bff 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -73,7 +73,7 @@ class UserTokensSaverTest { } coVerify(inverse = true) { - tangemTechApi.saveUserTokens(any(), any()) + tangemTechApi.saveTokens(any(), any()) } } @@ -108,7 +108,8 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse - coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse // WHEN userTokensSaver.push( @@ -120,7 +121,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" } @@ -155,7 +156,7 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } returns ApiResponse.Success(Unit) // WHEN @@ -164,7 +165,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } } } \ No newline at end of file diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index f7867f6b5d..5949dd0174 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -25,13 +25,12 @@ dependencies { implementation(projects.core.utils) /** Domain */ - implementation(projects.domain.wallets) + implementation(projects.domain.account) implementation(projects.domain.card) - api(projects.domain.models) - - /** Domain models */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /** DI */ implementation(deps.hilt.android) @@ -41,15 +40,15 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.retrofit) implementation(deps.timber) /** tests */ - testImplementation(projects.domain.models) testImplementation(projects.common.test) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) testImplementation(deps.test.mockk) - testImplementation(deps.moshi) - testImplementation(deps.moshi.kotlin) } \ No newline at end of file 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 982812c9b8..797d06f372 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 @@ -5,22 +5,22 @@ import arrow.core.left import arrow.core.right import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter -import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody -import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO +import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -import com.tangem.datasource.api.tangemTech.models.WalletBody -import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME import com.tangem.datasource.local.preferences.utils.* import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus @@ -30,13 +30,15 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map -@Suppress("TooManyFunctions", "LargeClass") +@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -44,6 +46,8 @@ internal class DefaultWalletsRepository( private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { private val upgradeWalletNotificationDisabled: MutableStateFlow> = @@ -388,24 +392,58 @@ internal class DefaultWalletsRepository( override suspend fun associateWallets(applicationId: String, wallets: List) = withContext(dispatchers.io) { - val publicKeys = authProvider.getCardsPublicKeys() - val walletsBody = wallets.map { userWallet -> - WalletIdBodyConverter.convert( - userWallet = userWallet, - publicKeys = if (userWallet is UserWallet.Cold) { - publicKeys.filterKeys { - userWallet.cardsInWallet.contains(it) - } - } else { - emptyMap() - }, - ) - } + if (accountsFeatureToggles.isFeatureEnabled) { + val associateApplicationIdWithWallets: suspend () -> ApiResponse = { + tangemTechApi.associateApplicationIdWithWalletsV2( + applicationId = applicationId, + body = AssociateApplicationIdWithWalletsBody( + walletIds = wallets.map { it.walletId.stringValue }.distinct(), + ), + ) + } - tangemTechApi.associateApplicationIdWithWallets( - applicationId = applicationId, - body = walletsBody, - ).getOrThrow() + val apiResponse = associateApplicationIdWithWallets() + + if (apiResponse is ApiResponse.Success) return@withContext + + if (apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) + ) { + val errorBody = (apiResponse.cause as? HttpException)?.errorBody + ?: error("Bad Request must have error body") + + val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) + val errorResponse = adapter.fromJson(errorBody) + ?: error("Cannot parse error body: $errorBody") + + errorResponse.missingWalletIds + .map { + async { createWallet(userWalletId = UserWalletId(it)) } + } + .awaitAll() + + associateApplicationIdWithWallets().getOrThrow() + } + } else { + val publicKeys = authProvider.getCardsPublicKeys() + val walletsBody = wallets.map { userWallet -> + WalletIdBodyConverter.convert( + userWallet = userWallet, + publicKeys = if (userWallet is UserWallet.Cold) { + publicKeys.filterKeys { + userWallet.cardsInWallet.contains(it) + } + } else { + emptyMap() + }, + ) + } + + tangemTechApi.associateApplicationIdWithWallets( + applicationId = applicationId, + body = walletsBody, + ).getOrThrow() + } } override suspend fun activatePromoCode( diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index bcaff10b6f..a1e870a3e1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,5 +1,6 @@ package com.tangem.data.wallets.di +import com.squareup.moshi.Moshi import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository @@ -8,9 +9,11 @@ import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository @@ -37,6 +40,8 @@ internal object WalletsDataModule { userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, + accountsFeatureToggles: AccountsFeatureToggles, + @NetworkMoshi moshi: Moshi, ): WalletsRepository { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, @@ -45,6 +50,8 @@ internal object WalletsDataModule { seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = moshi, ) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index e2a9090de4..80b3e8d659 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError @@ -51,6 +52,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), + accountsFeatureToggles = mockk(), + moshi = mockk(), ) } @@ -204,6 +207,10 @@ class DefaultWalletsRepositoryTest { coEvery { getCardsPublicKeys() } returns publicKeys } + val accountsFeatureToggles = mockk { + every { isFeatureEnabled } returns false + } + repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, @@ -211,6 +218,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = mockk(), ) coEvery { From 133c89dd7e3fdc61333bb0129a6f37efc811f5d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 15:22:15 +0400 Subject: [PATCH 039/130] Updated on 2026-08-14 --- .../fetcher/DefaultWalletAccountsFetcher.kt | 39 ++++++++++++++----- .../FetchWalletAccountsErrorHandler.kt | 10 ++--- .../FetchWalletAccountsErrorHandlerTest.kt | 12 +++--- 3 files changed, 38 insertions(+), 23 deletions(-) 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 3bc99dc784..8c475f3b58 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 @@ -94,21 +94,23 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( override suspend fun push( userWalletId: UserWalletId, body: SaveWalletAccountsResponse, + ): GetWalletAccountsResponse? { + return pushInternal(userWalletId = userWalletId, body = body) + } + + private suspend fun pushInternal( + userWalletId: UserWalletId, + body: SaveWalletAccountsResponse, + eTag: String? = null, ): GetWalletAccountsResponse? { return safeApiCall( call = { - var eTag = getETag(userWalletId) - - if (eTag == null) { - fetch(userWalletId) - - eTag = getETag(userWalletId) ?: error("ETag is null after fetch") - } + val resolvedETag = eTag ?: getETagForPush(userWalletId) val apiResponse = withContext(dispatchers.io) { tangemTechApi.saveWalletAccounts( walletId = userWalletId.stringValue, - eTag = eTag, + eTag = resolvedETag, body = body, ) } @@ -127,6 +129,19 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( ) } + private suspend fun getETagForPush(userWalletId: UserWalletId): String { + var savedETag = getETag(userWalletId) + + if (savedETag == null) { + fetch(userWalletId) + + savedETag = getETag(userWalletId) + ?: error("Failed to retrieve ETag after fetching wallet accounts for wallet $userWalletId") + } + + return savedETag + } + private suspend fun fetchWalletAccounts( userWalletId: UserWalletId, savedAccountsResponse: GetWalletAccountsResponse?, @@ -154,7 +169,13 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( error = throwable, userWalletId = userWalletId, savedAccountsResponse = savedAccountsResponse, - pushWalletAccounts = ::push, + pushWalletAccounts = { accounts, eTag -> + pushInternal( + userWalletId = userWalletId, + body = SaveWalletAccountsResponse(accounts), + eTag = eTag, + ) + }, storeWalletAccounts = ::store, ) }, diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 9e86b4848b..772a1dce68 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.fetcher import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory -import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse @@ -29,10 +28,10 @@ import javax.inject.Singleton * Handles errors that occur during the fetching of wallet accounts * * @property tangemTechApi API for network requests + * @property userWalletsStore provides access to user wallets storage * @property userTokensSaver saves user tokens to the storage * @property userTokensResponseStore provides access to user token responses. * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] - * @property eTagsStore store for ETags to manage caching * @property dispatchers dispatchers * * @see DefaultWalletAccountsFetcher @@ -47,7 +46,6 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, - private val eTagsStore: ETagsStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -66,7 +64,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( error: ApiResponseError, userWalletId: UserWalletId, savedAccountsResponse: GetWalletAccountsResponse?, - pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse?, + pushWalletAccounts: suspend (List, String) -> GetWalletAccountsResponse?, storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit, ): FetchResult { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) @@ -87,9 +85,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( val eTag = createWallet(userWalletId) if (eTag != null) { - eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag) - - pushWalletAccounts(userWalletId, accountDTOs) + pushWalletAccounts(accountDTOs, eTag) userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse) } } diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 88e44f4db4..51d9c2ad3f 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -3,7 +3,6 @@ package com.tangem.data.account.fetcher import com.tangem.data.account.converter.createGetWalletAccountsResponse import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory -import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -38,7 +37,6 @@ class FetchWalletAccountsErrorHandlerTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() - private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true) private val handler = FetchWalletAccountsErrorHandler( tangemTechApi = tangemTechApi, @@ -46,17 +44,18 @@ class FetchWalletAccountsErrorHandlerTest { userTokensSaver = userTokensSaver, userTokensResponseStore = userTokensResponseStore, defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, - eTagsStore = eTagsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - private val pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse = + private val pushWalletAccounts: suspend (List, String) -> GetWalletAccountsResponse = mockk(relaxed = true) private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) @BeforeEach fun setupEach() { clearMocks( + tangemTechApi, + userWalletsStore, userTokensSaver, userTokensResponseStore, defaultWalletAccountsResponseFactory, @@ -129,7 +128,7 @@ class FetchWalletAccountsErrorHandlerTest { ), ) } returns apiResponse - coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse + coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse // Act handler.handle( @@ -150,8 +149,7 @@ class FetchWalletAccountsErrorHandlerTest { walletType = WalletType.COLD, ), ) - eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue) - pushWalletAccounts(userWalletId, listOf(accountDTO)) + pushWalletAccounts(listOf(accountDTO), eTagValue) storeWalletAccounts(userWalletId, savedAccountsResponse) } From dffa8b2658c8392758b1d418a9203a9ff12b7e82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 16:51:34 +0300 Subject: [PATCH 040/130] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 9 +++++++ .../tangem/core/ui/message/dialog/Dialogs.kt | 24 +++++++++++++++++++ .../hotwallet/DefaultHotWalletRepository.kt | 9 +++++++ .../hotwallet/IsHotWalletCreationSupported.kt | 9 +++++++ .../repository/HotWalletRepository.kt | 4 ++++ domain/wallets/build.gradle.kts | 1 + .../wallets/builder/HotUserWalletBuilder.kt | 10 ++++++++ .../impl/build.gradle.kts | 1 + .../CreateWalletSelectionModel.kt | 12 ++++++++++ .../create-wallet-start/impl/build.gradle.kts | 1 + .../CreateWalletStartModel.kt | 10 ++++++++ .../CreateMobileWalletModel.kt | 20 +++++++++++++++- 12 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 57fd8da9ae..6eb397fb4e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module @@ -24,4 +25,12 @@ internal object HotWalletDomainModule { fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase { return SetAccessCodeSkippedUseCase(hotWalletRepository) } + + @Provides + @Singleton + fun provideIsWalletCreationSupportedUseCase( + hotWalletRepository: HotWalletRepository, + ): IsHotWalletCreationSupported { + return IsHotWalletCreationSupported(hotWalletRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index 08fb5a9e46..dec4c24085 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -39,6 +39,30 @@ object Dialogs { ) } + /** + * Hot wallet creation not supported dialog + * + * @param leastSupportedVersion least supported OS version name ex. "Android 10" + * @param onDismiss lambda be invoked when dialog is dismissed + */ + fun hotWalletCreationNotSupportedDialog(leastSupportedVersion: String, onDismiss: () -> Unit = {}): DialogMessage { + return DialogMessage( + title = resourceReference( + id = R.string.mobile_wallet_requires_min_os_warning_title, + formatArgs = wrappedList(leastSupportedVersion), + ), + message = resourceReference( + id = R.string.mobile_wallet_requires_min_os_warning_body, + formatArgs = wrappedList(leastSupportedVersion), + ), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = {}, + ), + onDismissRequest = onDismiss, + ) + } + /** * Universal error dialog */ diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index 4670884954..5754358efa 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -1,5 +1,7 @@ package com.tangem.data.hotwallet +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap @@ -12,6 +14,13 @@ internal class DefaultHotWalletRepository( private val appPreferencesStore: AppPreferencesStore, ) : HotWalletRepository { + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + override fun isWalletCreationSupported(): Boolean { + return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + } + + override fun getLeastSupportedAndroidVersionName(): String = "Android 10" + override fun accessCodeSkipped(userWalletId: UserWalletId): Flow = appPreferencesStore .getObjectMap(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY) .map { it[userWalletId.stringValue] == true } diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt new file mode 100644 index 0000000000..f3e4a3c05f --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository + +class IsHotWalletCreationSupported(private val hotWalletRepository: HotWalletRepository) { + operator fun invoke(): Boolean = hotWalletRepository.isWalletCreationSupported() + + fun getLeastVersionName(): String = hotWalletRepository.getLeastSupportedAndroidVersionName() +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 5bc636f2dd..e228cd4419 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -5,6 +5,10 @@ import kotlinx.coroutines.flow.Flow interface HotWalletRepository { + fun isWalletCreationSupported(): Boolean + + fun getLeastSupportedAndroidVersionName(): String + fun accessCodeSkipped(userWalletId: UserWalletId): Flow suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean) diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index ad9162e60a..a5779a9b3f 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications.models) implementation(projects.domain.demo.models) + implementation(projects.domain.hotWallet) // endregion // region Tangem libraries diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index eaa1c7dc7c..de81ddc111 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.domain.wallets.builder import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.models.MobileWallet import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase @@ -22,9 +23,12 @@ class HotUserWalletBuilder @AssistedInject constructor( private val hotSdk: TangemHotSdk, private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val dispatcherProvider: CoroutineDispatcherProvider, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { + checkHotWalletCreationSupported() + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> @@ -73,6 +77,12 @@ class HotUserWalletBuilder @AssistedInject constructor( ) } + fun checkHotWalletCreationSupported() { + require(isHotWalletCreationSupported()) { + "Hot wallet creation is supported only from ${isHotWalletCreationSupported.getLeastVersionName()}" + } + } + @AssistedFactory interface Factory { fun create(hotWalletId: HotWalletId): HotUserWalletBuilder diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index da58579502..4f1fb170a2 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.wallets) implementation(projects.domain.models) + implementation(projects.domain.hotWallet) /** Core modules */ implementation(projects.core.configToggles) 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 7e9fcbc7f5..ba67d8e99a 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 @@ -7,10 +7,13 @@ 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 +import com.tangem.core.decompose.ui.UiMessageSender 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.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.features.createwalletselection.impl.R @@ -31,6 +34,8 @@ internal class CreateWalletSelectionModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, + private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -93,6 +98,13 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onMobileWalletClick() { + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + return + } + router.push(AppRoute.CreateMobileWallet) } diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 6909f9d54c..8552f7b7d9 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.wallets) implementation(projects.domain.models) + implementation(projects.domain.hotWallet) /** Core modules */ implementation(projects.core.configToggles) 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 6e138c594d..9a74037170 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 @@ -17,11 +17,13 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog import com.tangem.domain.card.ScanCardProcessor 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 +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder @@ -52,6 +54,7 @@ internal class CreateWalletStartModel @Inject constructor( private val appRouter: AppRouter, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, private val userWalletsListRepository: UserWalletsListRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, @@ -131,6 +134,13 @@ internal class CreateWalletStartModel @Inject constructor( } private fun onStartWithMobileWalletClick() { + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + return + } + router.push(AppRoute.CreateMobileWallet) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 47d9334760..49a136ea47 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -5,6 +5,9 @@ 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 +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase @@ -32,6 +35,8 @@ internal class CreateMobileWalletModel @Inject constructor( private val router: Router, private val tangemHotSdk: TangemHotSdk, private val trackingContextProxy: TrackingContextProxy, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, + private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -54,10 +59,13 @@ internal class CreateMobileWalletModel @Inject constructor( } private fun onImportClick() { + checkHotWalletCreationSupported(notSupported = { return }) router.push(AppRoute.AddExistingWallet) } private fun onCreateClick() { + checkHotWalletCreationSupported(notSupported = { return }) + modelScope.launch { uiState.update { it.copy(createButtonLoading = true) @@ -70,9 +78,10 @@ internal class CreateMobileWalletModel @Inject constructor( saveUserWalletUseCase(userWallet) - launch(NonCancellable) { + launch(dispatchers.main + NonCancellable) { syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId) } + router.replaceAll(AppRoute.Wallet) }.onFailure { throwable -> Timber.e(throwable) @@ -81,4 +90,13 @@ internal class CreateMobileWalletModel @Inject constructor( } } } + + private inline fun checkHotWalletCreationSupported(notSupported: () -> Unit) { + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + notSupported() + } + } } \ No newline at end of file From 7f8592c0078c83a821d89a2af8890d771b6ebf54 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 19:20:12 +0300 Subject: [PATCH 041/130] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 7 +++ .../hotwallet/IsAccessCodeSimpleUseCase.kt | 13 ++++++ .../hotwallet/accesscode/AccessCodeModel.kt | 43 ++++++++++++++++--- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 6eb397fb4e..8c8404b2d3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported +import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module @@ -26,6 +27,12 @@ internal object HotWalletDomainModule { return SetAccessCodeSkippedUseCase(hotWalletRepository) } + @Provides + @Singleton + fun provideIsAccessCodeSimpleUseCase(): IsAccessCodeSimpleUseCase { + return IsAccessCodeSimpleUseCase() + } + @Provides @Singleton fun provideIsWalletCreationSupportedUseCase( diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt new file mode 100644 index 0000000000..04751ad562 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +class IsAccessCodeSimpleUseCase { + operator fun invoke(accessCode: String): Boolean { + return isSequential(accessCode) || isRepeatedCharacters(accessCode) + } + + fun isSequential(code: String): Boolean = code.length > 1 && + (code.zipWithNext().all { it.second == it.first + 1 } || + code.zipWithNext().all { it.second == it.first - 1 }) + + fun isRepeatedCharacters(code: String): Boolean = code.isNotEmpty() && code.all { it == code.first() } +} \ No newline at end of file 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 93c0b75006..595dff3477 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 @@ -11,6 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.CanUseBiometryUseCase @@ -56,6 +57,7 @@ internal class AccessCodeModel @Inject constructor( private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase, private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val isAccessCodeSimpleUseCase: IsAccessCodeSimpleUseCase, private val uiMessageSender: UiMessageSender, ) : Model() { @@ -117,16 +119,45 @@ internal class AccessCodeModel @Inject constructor( modelScope.launch { delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS) - params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode) - - uiState.update { currentState -> - currentState.copy( - accessCode = "", - ) + if (isAccessCodeSimpleUseCase(uiState.value.accessCode)) { + showSimpleAccessCodeDialog() + } else { + setNewCode() } } } + private fun setNewCode() { + params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode) + + uiState.update { currentState -> + currentState.copy( + accessCode = "", + ) + } + } + + private fun showSimpleAccessCodeDialog() { + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.access_code_alert_validation_title), + message = resourceReference(R.string.access_code_alert_validation_description), + firstAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_validation_cancel), + onClick = { + uiState.update { currentState -> + currentState.copy(onAccessCodeChange = ::onAccessCodeChange) + } + }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_validation_ok), + onClick = ::setNewCode, + ), + ), + ) + } + private suspend fun showErrorAndReset() { uiState.update { currentState -> currentState.copy( From 45b1b619fbf1b8897ac57cfa99cf4ef72c9f5875 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Dec 2025 11:38:41 +0000 Subject: [PATCH 042/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 07be659f64..8c10bcaf9d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31-1314" +tangemBlockchainSdk = "develop-1317" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +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 f2f10fe9501d376bae11b37991bce45601800992 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Dec 2025 17:22:28 +0500 Subject: [PATCH 043/130] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 6 -- .../tap/common/analytics/events/Onboarding.kt | 80 ------------------- .../domain/scanCard/LegacyScanProcessor.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 4 +- .../com/tangem/common/routing/AppRoute.kt | 4 +- .../core/analytics/models/AnalyticsParam.kt | 10 +-- .../models/event/OnboardingAnalyticsEvent.kt | 76 +++++++++++++++++- .../domain/nft/analytics/NFTAnalyticsEvent.kt | 10 +-- .../TokenReceiveNewAnalyticsEvent.kt | 2 +- .../CreateWalletSelectionModel.kt | 8 +- .../CreateWalletStartModel.kt | 8 +- .../hotwallet/CreateMobileWalletComponent.kt | 6 +- .../model/AddExistingWalletImportModel.kt | 23 ++++++ .../model/ImportSeedPhraseUiStateBuilder.kt | 3 + .../CreateMobileWalletModel.kt | 27 +++++++ .../DefaultCreateMobileWalletComponent.kt | 7 +- .../backup/model/MultiWalletBackupModel.kt | 2 +- .../v2/send/analytics/SendAnalyticEvents.kt | 6 +- .../v2/send/analytics/SendAnalyticHelper.kt | 6 +- .../model/TokenReceiveAssetsModel.kt | 6 +- .../intents/WalletContentClickIntents.kt | 5 +- .../analytics/WalletScreenAnalyticsEvent.kt | 2 +- .../utils/TokenListAnalyticsSender.kt | 4 +- 23 files changed, 182 insertions(+), 125 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index a90e193a92..125b159021 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -12,12 +12,6 @@ sealed class AnalyticsParam { class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) } - sealed class CardBalanceState(val value: String) { - data object Empty : CardBalanceState("Empty") - data object Full : CardBalanceState("Full") - companion object - } - sealed class RateApp(val value: String) { data object Liked : RateApp("Liked") data object Closed : RateApp("Close") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index a4fcb32fe0..a8deb0d88d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -11,85 +11,5 @@ sealed class Onboarding( params: Map = emptyMap(), ) : AnalyticsEvent(category, event, params) { - class Started : Onboarding("Onboarding", "Onboarding Started") class Finished : Onboarding("Onboarding", "Onboarding Finished") - - sealed class CreateWallet( - event: String, - params: Map = emptyMap(), - ) : Onboarding("Onboarding / Create Wallet", event, params) { - - class ScreenOpened : CreateWallet("Create Wallet Screen Opened") - class ButtonCreateWallet : CreateWallet("Button - Create Wallet") - class WalletCreatedSuccessfully( - creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey, - seedPhraseLength: Int? = null, - ) : CreateWallet( - event = "Wallet Created Successfully", - params = buildMap { - put(AnalyticsParam.CREATION_TYPE, creationType.value) - - if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString()) - }, - ) - } - - sealed class Backup( - event: String, - params: Map = emptyMap(), - ) : Onboarding("Onboarding / Backup", event, params) { - - class ScreenOpened : Backup("Backup Screen Opened") - class Started : Backup("Backup Started") - class Skipped : Backup("Backup Skipped") - class SettingAccessCodeStarted : Backup("Setting Access Code Started") - class AccessCodeEntered : Backup("Access Code Entered") - class AccessCodeReEntered : Backup("Access Code Re-entered") - - class Finished(cardsCount: Int) : Backup( - event = "Backup Finished", - params = mapOf("Cards count" to "$cardsCount"), - ) - - object ResetCancelEvent : Backup( - event = "Reset Card Notification", - params = mapOf("Option" to "Cancel"), - ) - - object ResetPerformEvent : Backup( - event = "Reset Card Notification", - params = mapOf("Option" to "Reset"), - ) - } - - sealed class Topup( - event: String, - params: Map = emptyMap(), - ) : Onboarding("Onboarding / Top Up", event, params) { - - class ScreenOpened : Topup("Activation Screen Opened") - - class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup( - event = "Button - Buy Crypto", - params = mapOf(AnalyticsParam.CURRENCY to currency.value), - ) - - class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") - } - - sealed class Twins( - event: String, - params: Map = emptyMap(), - ) : Onboarding("Onboarding / Twins", event, params) { - - class ScreenOpened : Twins("Twinning Screen Opened") - class SetupStarted : Twins("Twin Setup Started") - class SetupFinished : Twins("Twin Setup Finished") - } - - class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding( - category = "Onboarding / Biometric", - event = "Enable Biometric", - params = mapOf("State" to state.value), - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index ac18c8b943..95d2b8fe04 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -164,7 +164,7 @@ internal class LegacyScanProcessor @Inject constructor( ) { if (error is TangemSdkError.CardVerificationFailed) { analyticsEventHandler.send( - event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed( + event = OnboardingAnalyticsEvent.Error.OfflineAttestationFailed( analyticsSource, ), ) 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 ecb61b1097..df8495fe6c 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 @@ -529,7 +529,9 @@ internal class ChildFactory @Inject constructor( is AppRoute.CreateMobileWallet -> { createComponentChild( context = context, - params = Unit, + params = CreateMobileWalletComponent.Params( + source = route.source, + ), componentFactory = createMobileWalletComponentFactory, ) } 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 44386ece68..cbe8919317 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 @@ -346,7 +346,9 @@ sealed class AppRoute(val path: String) : Route { object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet") @Serializable - object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + data class CreateMobileWallet( + val source: String, + ) : AppRoute(path = "/create_mobile_wallet") @Serializable data class UpgradeWallet( 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 b0114af14c..7f051ebf87 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 @@ -11,11 +11,6 @@ sealed class AnalyticsParam { companion object } - sealed class TokenBalanceState(val value: String) { - data object Empty : TokenBalanceState("Empty") - data object Full : TokenBalanceState("Full") - } - sealed class RateApp(val value: String) { data object Liked : RateApp("Liked") data object Disliked : RateApp("Disliked") @@ -205,8 +200,9 @@ sealed class AnalyticsParam { Pending(value = "Pending"), } - enum class EnsStatus(val value: String) { - EMPTY("Empty"), FULL("Full") + enum class EmptyFull(val value: String) { + Empty("Empty"), + Full("Full"), } enum class ProductType(val value: String) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt index f60f3b53fe..d10949860f 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt @@ -12,11 +12,85 @@ sealed class OnboardingAnalyticsEvent( sealed class Onboarding( event: String, params: Map = mapOf(), + ) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) { + + class Started( + source: String, + ) : Onboarding( + event = "Onboarding Started", + params = mapOf( + AnalyticsParam.SOURCE to source, + ), + ) + + class Finished( + source: String, + ) : Onboarding( + event = "Onboarding Finished", + params = mapOf( + AnalyticsParam.SOURCE to source, + ), + ) + + class ButtonMobileWallet( + source: String, + ) : Onboarding( + event = "Button - Mobile Wallet", + params = mapOf( + AnalyticsParam.SOURCE to source, + ), + ) + } + + sealed class CreateWallet( + event: String, + params: Map = mapOf(), + ) : OnboardingAnalyticsEvent(category = "Onboarding / Create Wallet", event = event, params = params) { + + data object ButtonCreateWallet : CreateWallet("Button - Create Wallet") + + class WalletCreatedSuccessfully( + source: String, + creationType: WalletCreationType = WalletCreationType.NewSeed, + seedPhraseLength: Int? = null, + passPhraseState: AnalyticsParam.EmptyFull, + ) : CreateWallet( + event = "Wallet Created Successfully", + params = buildMap { + put(AnalyticsParam.SOURCE, source) + put("Creation Type", creationType.value) + put("Passphrase", passPhraseState.value) + if (seedPhraseLength != null) { + put("Seed Phrase Length", seedPhraseLength.toString()) + } + }, + ) + + sealed class WalletCreationType(val value: String) { + data object NewSeed : WalletCreationType(value = "New Seed") + data object SeedImport : WalletCreationType(value = "Seed Import") + } + } + + sealed class SeedPhrase( + event: String, + params: Map = mapOf(), + ) : OnboardingAnalyticsEvent(category = "Onboarding / Seed Phrase", event = event, params = params) { + + data object CreateMobileScreenOpened : SeedPhrase("Create Mobile Screen Opened") + data object ButtonImportWallet : SeedPhrase("Button - Import Wallet") + data object ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened") + data object ButtonImport : SeedPhrase("Button - Import") + } + + sealed class Error( + event: String, + params: Map = mapOf(), ) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) { data class OfflineAttestationFailed( val source: AnalyticsParam.ScreensSources, - ) : Onboarding( + ) : Error( event = "Offline Attestation Failed", params = mapOf(AnalyticsParam.SOURCE to source.value), ) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt index 7ccdadbd7d..0c0e7d83cd 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.nft.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.COLLECTIONS import com.tangem.core.analytics.models.AnalyticsParam.Key.NFT @@ -18,7 +19,7 @@ sealed class NFTAnalyticsEvent( ) { data class NFTListScreenOpened( - val state: State, + val state: AnalyticsParam.EmptyFull, val collectionsCount: Int, val allAssetsCount: Int, val noCollectionAssetsCount: Int, @@ -30,12 +31,7 @@ sealed class NFTAnalyticsEvent( put(NFT, allAssetsCount.toString()) put(NO_COLLECTION, noCollectionAssetsCount.toString()) }, - ) { - enum class State(val value: String) { - Empty("Empty"), - Full("Full"), - } - } + ) object Receive { data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt index cf752c7e38..5916c680aa 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt @@ -15,7 +15,7 @@ sealed class TokenReceiveNewAnalyticsEvent( class ReceiveScreenOpened( token: String, blockchainName: String, - ensStatus: AnalyticsParam.EnsStatus, + ensStatus: AnalyticsParam.EmptyFull, ) : TokenReceiveNewAnalyticsEvent( event = "Receive Screen Opened", params = mapOf( 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 ba67d8e99a..48a41b6f98 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 @@ -4,6 +4,7 @@ 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.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -98,6 +99,11 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onMobileWalletClick() { + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( + source = AnalyticsParam.ScreensSources.AddNewWallet.value, + ), + ) if (!isHotWalletCreationSupported()) { uiMessageSender.send( hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), @@ -105,7 +111,7 @@ internal class CreateWalletSelectionModel @Inject constructor( return } - router.push(AppRoute.CreateMobileWallet) + router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.AddNewWallet.value)) } private fun onHardwareWalletClick() { 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 9a74037170..c72cfae7a2 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 @@ -7,6 +7,7 @@ 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 +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -134,6 +135,11 @@ internal class CreateWalletStartModel @Inject constructor( } private fun onStartWithMobileWalletClick() { + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( + source = AnalyticsParam.ScreensSources.CreateNewWallet.value, + ), + ) if (!isHotWalletCreationSupported()) { uiMessageSender.send( hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), @@ -141,7 +147,7 @@ internal class CreateWalletStartModel @Inject constructor( return } - router.push(AppRoute.CreateMobileWallet) + router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateNewWallet.value)) } private fun onBuyClick() { diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt index e9bdbf64e5..72db10452d 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt @@ -4,5 +4,9 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface CreateMobileWalletComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params( + val source: String, + ) + + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 7d8156ecd3..d891de2892 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,5 +1,8 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -37,6 +40,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() @@ -60,6 +64,7 @@ internal class AddExistingWalletImportModel @Inject constructor( } init { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened) importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, mnemonicRepository = mnemonicRepository, @@ -72,6 +77,7 @@ internal class AddExistingWalletImportModel @Inject constructor( ) }, onPassphraseInfoClick = ::onPassphraseInfoClick, + onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport) }, ) } @@ -101,6 +107,23 @@ internal class AddExistingWalletImportModel @Inject constructor( } .onRight { setImportProgress(false) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.Finished( + source = AnalyticsParam.ScreensSources.ImportWallet.value, + ), + ) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully( + source = AnalyticsParam.ScreensSources.ImportWallet.value, + creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.SeedImport, + seedPhraseLength = mnemonic.mnemonicComponents.size, + passPhraseState = if (passphrase.isNullOrBlank()) { + AnalyticsParam.EmptyFull.Empty + } else { + AnalyticsParam.EmptyFull.Full + }, + ), + ) params.callbacks.onWalletImported(userWallet.walletId) } }.onFailure { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index 95e7f9c2db..cfdb385983 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class ImportSeedPhraseUiStateBuilder( private val modelScope: CoroutineScope, private val mnemonicRepository: MnemonicRepository, @@ -24,6 +25,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, private val onPassphraseInfoClick: () -> Unit, + private val onImportClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -57,6 +59,7 @@ internal class ImportSeedPhraseUiStateBuilder( } private fun onCreateWallet() { + onImportClick() val mnemonic = importedMnemonic ?: return val passphrase = passphrase?.takeIf { it.isNotEmpty() } importWallet(mnemonic, passphrase) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 49a136ea47..243343ce0b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -1,9 +1,13 @@ package com.tangem.features.hotwallet.createmobilewallet 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.event.OnboardingAnalyticsEvent 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.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog @@ -11,6 +15,7 @@ import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase +import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth @@ -28,6 +33,7 @@ import javax.inject.Inject @Suppress("LongParameterList") @ModelScoped internal class CreateMobileWalletModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, @@ -37,8 +43,11 @@ internal class CreateMobileWalletModel @Inject constructor( private val trackingContextProxy: TrackingContextProxy, private val isHotWalletCreationSupported: IsHotWalletCreationSupported, private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { + private val params: CreateMobileWalletComponent.Params = paramsContainer.require() + internal val uiState: StateFlow field = MutableStateFlow( CreateMobileWalletUM( @@ -51,6 +60,8 @@ internal class CreateMobileWalletModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Started(source = params.source)) + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened) } override fun onDestroy() { @@ -59,11 +70,13 @@ internal class CreateMobileWalletModel @Inject constructor( } private fun onImportClick() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet) checkHotWalletCreationSupported(notSupported = { return }) router.push(AppRoute.AddExistingWallet) } private fun onCreateClick() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet) checkHotWalletCreationSupported(notSupported = { return }) modelScope.launch { @@ -78,6 +91,16 @@ internal class CreateMobileWalletModel @Inject constructor( saveUserWalletUseCase(userWallet) + analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished(source = params.source)) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully( + source = params.source, + creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed, + seedPhraseLength = SEED_PHRASE_LENGTH, + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) + launch(dispatchers.main + NonCancellable) { syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId) } @@ -99,4 +122,8 @@ internal class CreateMobileWalletModel @Inject constructor( notSupported() } } + + companion object { + private const val SEED_PHRASE_LENGTH = 12 + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt index bf5fd61424..2dbd2f1aa4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt @@ -15,7 +15,7 @@ import dagger.assisted.AssistedInject @Suppress("UnusedPrivateMember") internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, - @Assisted private val params: Unit, + @Assisted private val params: CreateMobileWalletComponent.Params, ) : CreateMobileWalletComponent, AppComponentContext by context { private val model: CreateMobileWalletModel = getOrCreateModel(params) @@ -31,6 +31,9 @@ internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( @AssistedFactory interface Factory : CreateMobileWalletComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultCreateMobileWalletComponent + override fun create( + context: AppComponentContext, + params: CreateMobileWalletComponent.Params, + ): DefaultCreateMobileWalletComponent } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 4628feb506..609e1e7b6d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -250,7 +250,7 @@ class MultiWalletBackupModel @Inject constructor( private fun showCardVerificationFailedDialog(error: TangemSdkError.CardVerificationFailed) { analyticsEventHandler.send( - event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup), + event = OnboardingAnalyticsEvent.Error.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup), ) val resource = error.localizedDescriptionRes() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 4c2f12ab7a..5774e3b776 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -24,7 +24,7 @@ internal sealed class SendAnalyticEvents( val feeType: AnalyticsParam.FeeType, val blockchain: String, val nonceNotEmpty: Boolean, - private val ensStatus: AnalyticsParam.EnsStatus, + private val ensStatus: AnalyticsParam.EmptyFull, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = mapOf( @@ -33,8 +33,8 @@ internal sealed class SendAnalyticEvents( BLOCKCHAIN to blockchain, NONCE to nonceNotEmpty.toString().capitalize(), ENS_ADDRESS to when (ensStatus) { - AnalyticsParam.EnsStatus.EMPTY -> false.toString().capitalize() - AnalyticsParam.EnsStatus.FULL -> true.toString().capitalize() + AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() + AnalyticsParam.EmptyFull.Full -> true.toString().capitalize() }, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 0d5fe7f1e4..48f87023ed 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -53,13 +53,13 @@ internal class SendAnalyticHelper @Inject constructor( } } - private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus { + private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EmptyFull { val blockchainAddressForEns = (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.isAddressEns return if (blockchainAddressForEns == true) { - AnalyticsParam.EnsStatus.FULL + AnalyticsParam.EmptyFull.Full } else { - AnalyticsParam.EnsStatus.EMPTY + AnalyticsParam.EmptyFull.Empty } } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt index decfd54c50..50bc8ae792 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -56,12 +56,12 @@ internal class TokenReceiveAssetsModel @Inject constructor( ), ) - private fun configureEnsStatus(): AnalyticsParam.EnsStatus { + private fun configureEnsStatus(): AnalyticsParam.EmptyFull { val hasEnsAddress = params.addresses.any { it.type == ReceiveAddress.Type.Ens } return if (hasEnsAddress) { - AnalyticsParam.EnsStatus.FULL + AnalyticsParam.EmptyFull.Full } else { - AnalyticsParam.EnsStatus.EMPTY + AnalyticsParam.EmptyFull.Empty } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index b42d3aebb9..ca2353b273 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource 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.decompose.di.ModelScoped import com.tangem.domain.models.account.Account @@ -354,7 +355,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( is WalletNFTItemUM.Content -> { analyticsEventHandler.send( NFTAnalyticsEvent.NFTListScreenOpened( - state = NFTAnalyticsEvent.NFTListScreenOpened.State.Full, + state = AnalyticsParam.EmptyFull.Full, allAssetsCount = state.allAssetsCount, collectionsCount = state.collectionsCount, noCollectionAssetsCount = state.noCollectionAssetsCount, @@ -364,7 +365,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( is WalletNFTItemUM.Empty -> { analyticsEventHandler.send( NFTAnalyticsEvent.NFTListScreenOpened( - state = NFTAnalyticsEvent.NFTListScreenOpened.State.Empty, + state = AnalyticsParam.EmptyFull.Empty, allAssetsCount = 0, collectionsCount = 0, noCollectionAssetsCount = 0, 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 9f5263273e..00a748830b 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 @@ -37,7 +37,7 @@ sealed class WalletScreenAnalyticsEvent { }, ) - class TokenBalance(balance: AnalyticsParam.TokenBalanceState, token: String) : Basic( + class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", params = mapOf( AnalyticsParam.STATE to balance.value, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index fe583f7183..b8d4f81c17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -156,9 +156,9 @@ internal class TokenListAnalyticsSender @Inject constructor( -> { if (balanceWasSentMap[blockchain.currency] != true) { val tokenBalance = if (balanceStatus.amount.isZero()) { - AnalyticsParam.TokenBalanceState.Empty + AnalyticsParam.EmptyFull.Empty } else { - AnalyticsParam.TokenBalanceState.Full + AnalyticsParam.EmptyFull.Full } analyticsEventHandler.send( Basic.TokenBalance( From 2396225311bf3a771f77be0c745bfb723fefd787 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Dec 2025 14:34:56 +0200 Subject: [PATCH 044/130] Updated on 2026-08-14 --- build.gradle.kts | 88 ++++++++++++++++++++++++++++++++++++++++------- fastlane/Fastfile | 2 +- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b59181ab1b..c8864ff496 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import java.util.concurrent.ConcurrentHashMap plugins { alias(deps.plugins.kotlin.android) apply false @@ -32,21 +33,53 @@ interface Injected { val fs: FileSystemOperations } -// Test Logging +data class TestStats( + val total: Long = 0, + val passed: Long = 0, + val failed: Long = 0, + val skipped: Long = 0, +) + +val testResultsByModule = ConcurrentHashMap() + +// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules +val unitTest by tasks.registering { + group = "verification" + description = "Run unit tests for debug/googleDebug variant and all JVM modules" + + doLast { + if (testResultsByModule.isNotEmpty()) { + val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats -> + TestStats( + total = acc.total + stats.total, + passed = acc.passed + stats.passed, + failed = acc.failed + stats.failed, + skipped = acc.skipped + stats.skipped, + ) + } + + println("\n" + "=".repeat(80)) + println("TEST SUMMARY") + println("=".repeat(80)) + + testResultsByModule.toSortedMap().forEach { (module, stats) -> + println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)") + } + + println("-".repeat(80)) + println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules") + println(" Passed: ${totalStats.passed}") + println(" Failed: ${totalStats.failed}") + println(" Skipped: ${totalStats.skipped}") + println("=".repeat(80)) + } + } +} + +// Test Logging and testCI dependencies subprojects { tasks.withType().configureEach { - val taskName = name.lowercase() - if (taskName.contains("external") || - taskName.contains("internal") || - taskName.contains("release") || - taskName.contains("mocked") || - taskName.contains("huawei") - ) { - enabled = false - println("Skipping test task: $name") - } else { - println("Test task scheduled: $name") - } + println("Test task scheduled: $path") testLogging { exceptionFormat = TestExceptionFormat.FULL @@ -54,6 +87,13 @@ subprojects { afterSuite(KotlinClosure2({ desc, result -> if (desc.parent == null) { // will match the outermost suite + testResultsByModule[path] = TestStats( + total = result.testCount, + passed = result.successfulTestCount, + failed = result.failedTestCount, + skipped = result.skippedTestCount, + ) + val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" val startItem = "| " @@ -68,6 +108,28 @@ subprojects { })) } } + + // Register testCI dependencies + // App module + plugins.withId("com.android.application") { + afterEvaluate { + unitTest.configure { dependsOn(tasks.named("testGoogleDebugUnitTest")) } + } + } + + // Android libraries + plugins.withId("com.android.library") { + afterEvaluate { + unitTest.configure { dependsOn(tasks.named("testDebugUnitTest")) } + } + } + + // Jvm modules + plugins.withId("org.jetbrains.kotlin.jvm") { + if (!plugins.hasPlugin("com.android.library") && !plugins.hasPlugin("com.android.application")) { + unitTest.configure { dependsOn(tasks.named("test")) } + } + } } val assembleInternalQA by tasks.registering { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0b4ac32038..b62893f38d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -30,7 +30,7 @@ platform :android do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") - gradle(task: "test") + gradle(task: "unitTest") end desc "Build internal APK Firebase App Distribution" From 23ddf1a6659b86308dabb35423b17833d6bb9a01 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Dec 2025 17:38:17 +0500 Subject: [PATCH 045/130] Updated on 2026-08-14 --- .../com/tangem/common/ui/news/ArticleBadge.kt | 76 ----- .../com/tangem/common/ui/news/ArticleCard.kt | 83 ++---- .../tangem/common/ui/news/ArticleConfigUM.kt | 3 +- .../tangem/common/ui/news/ArticleHeader.kt | 55 ++++ .../com/tangem/common/ui/news/ArticleInfo.kt | 55 ++++ .../com/tangem/common/ui/news/ArticleTagUM.kt | 18 -- .../com/tangem/core/ui/components/Buttons.kt | 8 + .../tangem/core/ui/components/label/Label.kt | 173 ++++++++--- .../ui/components/label/entity/LabelUM.kt | 15 +- .../ui/components/pager/PagerIndicator.kt | 130 ++++++++ .../src/main/res/drawable/ic_explore_16.xml | 12 + core/ui/src/main/res/drawable/ic_heart_20.xml | 15 + .../main/res/drawable/ic_quick_recap_16.xml | 15 + .../ui/CreateWalletSelectionContent.kt | 8 +- .../preview/FeedListPreviewDataProvider.kt | 26 +- .../hotwallet/common/ui/OptionBlock.kt | 8 +- .../news/news-details/api/build.gradle.kts | 18 ++ .../news/details/api/NewsDetailsComponent.kt | 11 + .../news/news-details/impl/build.gradle.kts | 43 +++ .../impl/DefaultNewsDetailsComponent.kt | 39 +++ .../news/details/impl/MockArticlesFactory.kt | 182 ++++++++++++ .../news/details/impl/NewsDetailsModel.kt | 41 +++ .../news/details/impl/di/NewsDetailsModule.kt | 18 ++ .../details/impl/ui/NewsDetailsContent.kt | 278 ++++++++++++++++++ .../news/details/impl/ui/NewsDetailsUM.kt | 33 +++ features/tester/impl/build.gradle.kts | 2 + .../tester/presentation/TesterActivity.kt | 32 +- .../presentation/menu/state/TesterMenuUM.kt | 1 + .../presentation/navigation/TesterScreen.kt | 2 + .../tester/presentation/news/state/NewsUM.kt | 16 + .../tester/presentation/news/ui/NewsScreen.kt | 62 ++++ .../news/viewmodel/NewsViewModel.kt | 44 +++ .../impl/src/main/res/values/strings.xml | 2 + settings.gradle.kts | 3 + 34 files changed, 1301 insertions(+), 226 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt create mode 100644 core/ui/src/main/res/drawable/ic_explore_16.xml create mode 100644 core/ui/src/main/res/drawable/ic_heart_20.xml create mode 100644 core/ui/src/main/res/drawable/ic_quick_recap_16.xml create mode 100644 features/news/news-details/api/build.gradle.kts create mode 100644 features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt create mode 100644 features/news/news-details/impl/build.gradle.kts create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt create mode 100644 features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt deleted file mode 100644 index 157a36e08d..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.common.ui.news - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.currency.icon.CurrencyIcon -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .heightIn(min = 24.dp) - .background( - color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - when (articleTagUM) { - is ArticleTagUM.Category -> Unit - is ArticleTagUM.Token -> { - CurrencyIcon( - state = articleTagUM.iconState, - shouldDisplayNetwork = false, - iconSize = 16.dp, - ) - } - } - Text( - text = articleTagUM.title.resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.secondary, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ArticleBadgePreview() { - TangemThemePreview { - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - ArticleBadge( - articleTagUM = ArticleTagUM.Token( - TextReference.Str("BTC"), - iconState = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - ), - ) - ArticleBadge( - articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")), - ) - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 259c72b98d..a1ee6a610c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -1,7 +1,6 @@ package com.tangem.common.ui.news import android.content.res.Configuration -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -12,24 +11,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.collections.immutable.toPersistentList @Composable fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { @@ -121,54 +119,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) { } } -@Composable -private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) { - val dotColor = TangemTheme.colors.text.secondary - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12), - contentDescription = null, - ) - - Text( - text = score.toString(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, - ) - - Spacer( - modifier = Modifier - .size(4.dp) - .drawWithCache { - val radius = size.minDimension / 2f - onDrawBehind { - drawCircle( - color = dotColor, - radius = radius, - ) - } - }, - ) - - Text( - text = createdAt, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, - ) - } -} - @OptIn(ExperimentalLayoutApi::class) @Composable -private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { +private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { val expandIndicator = remember { ContextualFlowRowOverflow.expandIndicator { val remainingItems = tags.size - shownItemCount - ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) + Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) } } ContextualFlowRow( @@ -179,7 +136,7 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie maxLines = 1, overflow = expandIndicator, ) { index -> - ArticleBadge(articleTagUM = tags[index]) + Label(state = tags[index]) } } @@ -189,14 +146,14 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie private fun TagsPreview() { TangemThemePreview { Tags( - tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), - ).toPersistentList(), + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), ) } } @@ -206,12 +163,12 @@ private fun TagsPreview() { @Composable private fun ArticleCardsPreview() { val tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), ).toImmutableSet() val config = ArticleConfigUM( diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt index e6e9477790..46e616eaab 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.news +import com.tangem.core.ui.components.label.entity.LabelUM import kotlinx.collections.immutable.ImmutableSet data class ArticleConfigUM( @@ -8,6 +9,6 @@ data class ArticleConfigUM( val score: Float, val createdAt: String, val isTrending: Boolean, - val tags: ImmutableSet, + val tags: ImmutableSet, val isViewed: Boolean, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt new file mode 100644 index 0000000000..15bac87bdc --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.news + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ArticleHeader( + title: String, + createdAt: String, + score: Float, + tags: ImmutableList, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + ArticleInfo( + score = score, + createdAt = createdAt, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = title, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + if (tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + tags.forEach { tag -> + Label( + state = tag, + ) + } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt new file mode 100644 index 0000000000..f7cc656eae --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.news + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) { + val dotColor = TangemTheme.colors.text.secondary + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12), + contentDescription = null, + ) + + Text( + text = score.toString(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + + Spacer( + modifier = Modifier + .size(4.dp) + .drawWithCache { + val radius = size.minDimension / 2f + onDrawBehind { + drawCircle( + color = dotColor, + radius = radius, + ) + } + }, + ) + + Text( + text = createdAt, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt deleted file mode 100644 index 24eb867ad2..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.common.ui.news - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.TextReference - -@Immutable -sealed interface ArticleTagUM { - - val title: TextReference - - data class Category(override val title: TextReference) : ArticleTagUM - - data class Token( - override val title: TextReference, - val iconState: CurrencyIconState, - ) : ArticleTagUM -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7b4e6dbb70..00b71108b4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } @@ -244,6 +248,8 @@ fun SecondaryButtonIconStart( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -254,6 +260,8 @@ fun SecondaryButtonIconStart( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } // endregion SecondaryButton diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 2cc6209978..36e1eae057 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -17,11 +17,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference @@ -37,6 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview * * @see Figma */ +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun Label(state: LabelUM, modifier: Modifier = Modifier) { val backgroundColor by animateColorAsState( @@ -63,12 +68,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { }, ) - AnimatedContent(targetState = state.text) { text -> + val horizontalArrangementSize = remember { + when (state.size) { + LabelSize.REGULAR -> 4.dp + LabelSize.BIG -> 8.dp + } + } + + val paddings = remember { + when (state.size) { + LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp) + LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp) + } + } + + AnimatedContent( + modifier = modifier, + targetState = state.text, + ) { text -> Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .padding(horizontal = 4.dp) + horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize), + modifier = Modifier .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( @@ -82,8 +103,22 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { Modifier }, ) - .padding(horizontal = 8.dp, vertical = 4.dp), + .padding(paddings), ) { + state.leadingContent.let { leadingContentUM -> + when (leadingContentUM) { + is LabelLeadingContentUM.Token -> { + Icon( + modifier = Modifier + .size(16.dp), + painter = painterResource(leadingContentUM.icon), + tint = Color.Unspecified, + contentDescription = null, + ) + } + LabelLeadingContentUM.None -> Unit + } + } Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), @@ -109,6 +144,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -118,47 +155,93 @@ private fun LabelPreview() { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp), ) { - Label( - state = LabelUM( - text = TextReference.Str("Regular Label"), - style = LabelStyle.REGULAR, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str( - "Regular long long long long long long long long long long long long Label", + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, ), - style = LabelStyle.REGULAR, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - icon = R.drawable.ic_information_24, - ), - ) + ) + Label( + state = LabelUM( + leadingContent = LabelLeadingContentUM.Token( + icon = R.drawable.img_bsc_22, + ), + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str( + "Regular long long long long long long long long long long long long Label", + ), + style = LabelStyle.REGULAR, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + icon = R.drawable.ic_information_24, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + size = LabelSize.BIG, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + size = LabelSize.BIG, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + size = LabelSize.BIG, + ), + ) + } } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt index 9982a897b0..afa0e60bae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -1,16 +1,29 @@ package com.tangem.core.ui.components.label.entity import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference data class LabelUM( val text: TextReference, - val style: LabelStyle, + val style: LabelStyle = LabelStyle.REGULAR, + val size: LabelSize = LabelSize.REGULAR, + val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None, @DrawableRes val icon: Int? = null, val onIconClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null, ) +@Immutable +sealed class LabelLeadingContentUM { + data object None : LabelLeadingContentUM() + data class Token(val icon: Int) : LabelLeadingContentUM() +} + enum class LabelStyle { REGULAR, ACCENT, WARNING, +} + +enum class LabelSize { + REGULAR, BIG, } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt new file mode 100644 index 0000000000..8dc43f2d53 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -0,0 +1,130 @@ +package com.tangem.core.ui.components.pager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Horizontal pager indicator + * + * @param pagerState state of pager + * @param indicatorCount counter of visible indicator items + */ +@Composable +fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) { + val listState = rememberLazyListState() + + val indicatorColor = TangemTheme.colors.control.key + val overlayColor = TangemTheme.colors.overlay.secondary + val indicatorSize = 8.dp + val spacing = 4.dp + + val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1) + val widthInPx = LocalDensity.current.run { indicatorSize.toPx() } + + val currentItem by remember { + derivedStateOf { + pagerState.currentPage + } + } + + val itemCount = pagerState.pageCount + + LaunchedEffect(key1 = currentItem) { + val viewportSize = listState.layoutInfo.viewportSize + listState.animateScrollToItem( + currentItem, + (widthInPx / 2 - viewportSize.width / 2).toInt(), + ) + } + + Box( + modifier = modifier + .height(32.dp) + .background( + color = overlayColor, + shape = CircleShape, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + contentAlignment = Alignment.Center, + ) { + LazyRow( + modifier = Modifier + .width(totalWidth), + state = listState, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + userScrollEnabled = false, + ) { + indicatorItems( + itemCount = itemCount, + currentItem = currentItem, + indicatorShape = CircleShape, + activeColor = indicatorColor, + inActiveColor = indicatorColor.copy(alpha = 0.5f), + indicatorSize = indicatorSize, + ) + } + } +} + +@Suppress("LongParameterList") +private fun LazyListScope.indicatorItems( + itemCount: Int, + currentItem: Int, + indicatorShape: Shape, + activeColor: Color, + inActiveColor: Color, + indicatorSize: Dp, +) { + items(itemCount) { index -> + + val isSelected = index == currentItem + + Box( + modifier = Modifier + .clip(indicatorShape) + .size(indicatorSize) + .background( + if (isSelected) activeColor else inActiveColor, + indicatorShape, + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreviewFirstPage() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(), + contentAlignment = Alignment.Center, + ) { + val pagerState = rememberPagerState( + initialPage = 0, + pageCount = { 10 }, + ) + PagerIndicator(pagerState = pagerState) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_explore_16.xml b/core/ui/src/main/res/drawable/ic_explore_16.xml new file mode 100644 index 0000000000..a739a875d4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_explore_16.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_heart_20.xml b/core/ui/src/main/res/drawable/ic_heart_20.xml new file mode 100644 index 0000000000..6e0f036179 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_heart_20.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_quick_recap_16.xml b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml new file mode 100644 index 0000000000..11a259eed3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 7fde4ee3ac..07cdb0689e 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -137,11 +138,12 @@ private fun WalletBlock( vertical = 12.dp, ), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 8.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 734644fa3d..9eaf7e5c4b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -4,12 +4,13 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ArticleTagUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -159,21 +160,18 @@ internal object FeedListPreviewDataProvider { ), ) - private fun createArticleTags(): ImmutableSet { + private fun createArticleTags(): ImmutableSet { return persistentSetOf( - ArticleTagUM.Token( - title = TextReference.Str("BTC"), - iconState = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, + LabelUM( + text = TextReference.Str("BTC"), + leadingContent = LabelLeadingContentUM.Token( + icon = R.drawable.img_btc_22, ), ), - ArticleTagUM.Category(TextReference.Str("Regulation")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Regulation")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index f76ab84370..6f7f584bfb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.common.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -43,11 +44,12 @@ internal fun OptionBlock( } .padding(16.dp), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 4.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/news/news-details/api/build.gradle.kts b/features/news/news-details/api/build.gradle.kts new file mode 100644 index 0000000000..b68f6815c6 --- /dev/null +++ b/features/news/news-details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.api" +} + +dependencies { + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} diff --git a/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt new file mode 100644 index 0000000000..167f65b88f --- /dev/null +++ b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.news.details.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface NewsDetailsComponent : ComposableContentComponent { + + data class Params(val selectedArticleId: Int = 0) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/news/news-details/impl/build.gradle.kts b/features/news/news-details/impl/build.gradle.kts new file mode 100644 index 0000000000..a9953868af --- /dev/null +++ b/features/news/news-details/impl/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.impl" +} + +dependencies { + /* AndroidX */ + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.decompose) + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Feature modules */ + implementation(projects.features.news.newsDetails.api) + implementation(projects.domain.models) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt new file mode 100644 index 0000000000..d68562beb6 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.news.details.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNewsDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: NewsDetailsComponent.Params, +) : NewsDetailsComponent, AppComponentContext by context { + + private val model: NewsDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val uiState by model.uiState.collectAsState() + NewsDetailsContent( + state = uiState, + onBackClick = model::onBackClick, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : NewsDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NewsDetailsComponent.Params, + ): DefaultNewsDetailsComponent + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt new file mode 100644 index 0000000000..ac35bb6ee4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt @@ -0,0 +1,182 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.ui.components.label.entity.LabelSize +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.news.details.impl.ui.ArticleUM +import com.tangem.features.news.details.impl.ui.SourceUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +// TODO [REDACTED_TASK_KEY] remove mock data +@Suppress("MaximumLineLength", "LongMethod") +object MockArticlesFactory { + fun createMockArticles(): ImmutableList = listOf( + ArticleUM( + id = 1, + title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds", + createdAt = "20 Jun, 21:45", + score = 6.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitwise has updated its Solana ETF, adding staking and setting a low management fee of 0.20%.", + content = "Bitwise Asset Management has revised its filing to launch a Solana ETF, renaming it \"Bitwise Solana Staking ETF\" and setting an exceptionally low management fee of just 0.20%.\n\nThe update comes as the SEC prepares to review several Solana ETF applications.", + sources = listOf( + SourceUM( + id = 1, + title = "Deeper liquidity could drive crypto market beyond \$6T", + sourceName = "cointelegraph", + publishedAt = "1h ago", + url = "https://cointelegraph.com", + ), + SourceUM( + id = 2, + title = "Top gainers and losers in crypto this week", + sourceName = "Investing", + publishedAt = "2h ago", + url = "https://investing.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 2, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + createdAt = "20 Jun, 20:15", + score = 8.2f, + tags = listOf( + LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("ETF"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitcoin spot ETFs recorded fourth consecutive day of positive inflows.", + content = "Bitcoin spot ETFs continue their impressive streak with \$550 million in net positive inflows.\n\nBlackRock's IBIT led with \$250M.", + sources = listOf( + SourceUM( + id = 3, + title = "Bitcoin ETFs see massive inflows", + sourceName = "Bloomberg", + publishedAt = "3h ago", + url = "https://bloomberg.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 3, + title = "Ethereum network upgrade scheduled for Q2 2025", + createdAt = "20 Jun, 18:30", + score = 7.8f, + tags = listOf( + LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Ethereum developers announced major network upgrade.", + content = "The Ethereum Foundation announced a significant upgrade for Q2 2025.\n\nKey improvements include EVM enhancements.", + sources = listOf( + SourceUM( + id = 4, + title = "Ethereum core devs announce upgrade", + sourceName = "CoinDesk", + publishedAt = "5h ago", + url = "https://coindesk.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 4, + title = "Solana surpasses Ethereum in daily transaction volume", + createdAt = "20 Jun, 16:00", + score = 9.1f, + tags = listOf( + LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Solana achieved new milestone processing more daily transactions than Ethereum.", + content = "Solana processed over 50 million transactions in a single day.\n\nDriven by DeFi and NFT activity.", + sources = listOf( + SourceUM( + id = 5, + title = "Solana transactions hit record", + sourceName = "The Block", + publishedAt = "7h ago", + url = "https://theblock.co", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 5, + title = "DeFi protocol launches innovative yield farming strategy", + createdAt = "20 Jun, 14:20", + score = 6.9f, + tags = listOf( + LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New DeFi protocol introduced innovative yield farming approach.", + content = "A newly launched protocol unveiled innovative yield farming mechanism.\n\nAPY rates range from 15% to 30%.", + sources = persistentListOf(), + ), + ArticleUM( + id = 6, + title = "Crypto regulation bill advances in US Senate", + createdAt = "20 Jun, 12:45", + score = 8.7f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("USA"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Comprehensive cryptocurrency regulation bill passed Senate Banking Committee.", + content = "The US Senate Banking Committee advanced landmark crypto regulation bill.\n\nKey provisions include asset definitions.", + sources = persistentListOf(), + ), + ArticleUM( + id = 7, + title = "Major bank announces crypto custody services", + createdAt = "20 Jun, 10:30", + score = 7.3f, + tags = listOf( + LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "World's largest bank announced cryptocurrency custody services.", + content = "Major financial institution announced comprehensive crypto custody services.\n\nSupporting Bitcoin and Ethereum initially.", + sources = persistentListOf(), + ), + ArticleUM( + id = 8, + title = "NFT marketplace reports 300% increase in trading volume", + createdAt = "20 Jun, 08:15", + score = 5.8f, + tags = listOf( + LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Leading NFT marketplace experienced dramatic surge in trading activity.", + content = "Prominent NFT marketplace reported 300% increase in trading volume.\n\nNew features include lower fees.", + sources = persistentListOf(), + ), + ArticleUM( + id = 9, + title = "Layer 2 solution achieves 100,000 TPS milestone", + createdAt = "19 Jun, 22:00", + score = 8.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("L2"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New Layer 2 scaling solution achieved 100,000 TPS in testing.", + content = "Layer 2 solution processed 100,000 transactions per second.\n\nUsing zero-knowledge proof technology.", + sources = persistentListOf(), + ), + ArticleUM( + id = 10, + title = "Stablecoin market cap reaches new all-time high", + createdAt = "19 Jun, 19:30", + score = 7.6f, + tags = listOf( + LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Total stablecoin market capitalization surpassed \$180 billion.", + content = "Stablecoin market cap reached \$180 billion all-time high.\n\nDriven by DeFi activity and institutional adoption.", + sources = persistentListOf(), + ), + ).toPersistentList() +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt new file mode 100644 index 0000000000..3ff89dde9d --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.indexOfFirstOrNull +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@ModelScoped +internal class NewsDetailsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + private val mockedArticles = MockArticlesFactory.createMockArticles() + + private val params = paramsContainer.require() + + private val _uiState = MutableStateFlow( + NewsDetailsUM( + /* [REDACTED_TODO_COMMENT] */ + articles = mockedArticles, + selectedArticleIndex = mockedArticles.indexOfFirstOrNull { it.id == params.selectedArticleId } ?: 0, + onShareClick = { /* [REDACTED_TODO_COMMENT] */ }, + onLikeClick = { /* [REDACTED_TODO_COMMENT] */ }, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onBackClick() { + router.pop() + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt new file mode 100644 index 0000000000..f0fcac783e --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.news.details.impl.di + +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.DefaultNewsDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface NewsDetailsModule { + + @Binds + @Singleton + fun bindNewsDetailsComponentFactory(factory: DefaultNewsDetailsComponent.Factory): NewsDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt new file mode 100644 index 0000000000..a149f57bf4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt @@ -0,0 +1,278 @@ +package com.tangem.features.news.details.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.ArticleHeader +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.pager.PagerIndicator +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.news.details.impl.MockArticlesFactory + +// TODO [REDACTED_TASK_KEY] make internal +@Composable +fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val pagerState = rememberPagerState( + initialPage = state.selectedArticleIndex, + pageCount = { state.articles.size }, + ) + + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .systemBarsPadding(), + ) { + Column { + TangemTopAppBar( + title = null, + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = onBackClick, + ), + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_share_24, + onClicked = state.onShareClick, + ), + ) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.articles.isNotEmpty()) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + ArticleDetail( + article = state.articles[page], + modifier = Modifier.fillMaxSize(), + onLikeClick = state.onLikeClick, + ) + } + + if (state.articles.size > 1) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) { + PagerIndicator( + pagerState = pagerState, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + ) + } + } + } + } + } + } +} + +@Suppress("LongMethod") +@Composable +private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) { + val density = LocalDensity.current + val pagerHeight = 48.dp + val contentPadding = 56.dp + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + contentPadding = PaddingValues( + bottom = contentPadding + pagerHeight + WindowInsets.navigationBars.getBottom(density).dp, + ), + ) { + item { + ArticleHeader( + title = article.title, + createdAt = article.createdAt, + score = article.score, + tags = article.tags, + modifier = Modifier.padding(top = 16.dp), + ) + + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier.padding(top = 32.dp), + ) + } + + Text( + text = article.content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = 16.dp), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + SecondaryButtonIconStart( + iconResId = R.drawable.ic_heart_20, + text = "Like", // TODO [REDACTED_TASK_KEY] export to strings + size = TangemButtonSize.RoundedAction, + onClick = onLikeClick, + ) + + // TODO [REDACTED_TASK_KEY] add related tokens block + + if (article.sources.isNotEmpty()) { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Text( + text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "${article.sources.size}", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + if (article.sources.isNotEmpty()) { + item { + val sourcesPagerState = rememberPagerState( + pageCount = { article.sources.size }, + ) + Spacer(modifier = Modifier.height(12.dp)) + HorizontalPager( + state = sourcesPagerState, + modifier = Modifier + .fillMaxWidth(), + pageSpacing = 12.dp, + contentPadding = PaddingValues(horizontal = 0.dp), + ) { page -> + SourceItem( + source = article.sources[page], + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} + +@Composable +private fun QuickRecap(content: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(IntrinsicSize.Min), + ) { + VerticalDivider( + modifier = Modifier.fillMaxHeight(), + thickness = 2.dp, + color = TangemTheme.colors.stroke.primary, + ) + Column( + modifier = Modifier + .padding(start = 16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_quick_recap_16), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.accent, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(12.dp), + ) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_explore_16), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = source.sourceName, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } + if (source.title.isNotEmpty()) { + Text( + text = source.title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + Text( + text = source.publishedAt, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewNewsDetailsContent() { + TangemThemePreview { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onShareClick = { }, + onLikeClick = { }, + ), + onBackClick = { }, + ) + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt new file mode 100644 index 0000000000..889fcc202a --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt @@ -0,0 +1,33 @@ +package com.tangem.features.news.details.impl.ui + +import com.tangem.core.ui.components.label.entity.LabelUM +import kotlinx.collections.immutable.ImmutableList + +// TODO [REDACTED_TASK_KEY] make internal +data class NewsDetailsUM( + val articles: ImmutableList, + val selectedArticleIndex: Int, + val onShareClick: () -> Unit, + val onLikeClick: () -> Unit, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class ArticleUM( + val id: Int, + val title: String, + val createdAt: String, + val score: Float, + val tags: ImmutableList, + val shortContent: String, + val content: String, + val sources: ImmutableList, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class SourceUM( + val id: Int, + val title: String, + val sourceName: String, + val publishedAt: String, + val url: String, +) \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 57fe70672c..ba40b749c4 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { /** Feature Apis */ implementation(projects.features.tester.api) implementation(projects.features.pushNotifications.api) + implementation(projects.features.news.newsDetails.api) + implementation(projects.features.news.newsDetails.impl) /* SDK */ implementation(tangemDeps.blockchain) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index bab7b098d3..393fac33ad 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -35,6 +35,11 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel +import com.tangem.feature.tester.presentation.news.ui.NewsScreen +import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel +import com.tangem.features.news.details.impl.MockArticlesFactory +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM import dagger.hilt.android.AndroidEntryPoint import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -82,9 +87,10 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS, ButtonUM.TEST_PUSHES, ButtonUM.ACCOUNTS, + ButtonUM.NEWS, ), - onButtonClick = { - val route = when (it) { + onButtonClick = { buttonUM -> + val route = when (buttonUM) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES @@ -92,6 +98,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS + ButtonUM.NEWS -> TesterScreen.NEWS } innerTesterRouter.open(route) @@ -162,6 +169,27 @@ internal class TesterActivity : ComposeActivity() { AccountsScreen(state) } + + composable(route = TesterScreen.NEWS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + NewsScreen(state) + } + + composable(route = TesterScreen.NEWS_DETAILS.name) { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onLikeClick = { }, + onShareClick = { }, + ), + onBackClick = { innerTesterRouter.back() }, + ) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index fa30058309..00a019e9c4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -25,5 +25,6 @@ data class TesterMenuUM( TESTER_ACTIONS(R.string.tester_actions), TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), + NEWS(R.string.news), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index e87d942c19..854706cec2 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -14,4 +14,6 @@ internal enum class TesterScreen { BLOCKCHAIN_PROVIDERS, TEST_PUSHES, ACCOUNTS, + NEWS, + NEWS_DETAILS, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt new file mode 100644 index 0000000000..b832b363d8 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.tester.presentation.news.state + +import androidx.annotation.StringRes +import com.tangem.feature.tester.impl.R +import kotlinx.collections.immutable.ImmutableSet + +data class NewsUM( + val onBackClick: () -> Unit, + val buttons: ImmutableSet, + val onButtonClick: (ButtonUM) -> Unit, +) { + + enum class ButtonUM(@StringRes val textResId: Int) { + NEWS_DETAILS(R.string.news_details), + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt new file mode 100644 index 0000000000..2f0b9c690c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.tester.presentation.news.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.news.state.NewsUM +import kotlinx.collections.immutable.ImmutableSet + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun NewsScreen(state: NewsUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { AppBar(onBackClick = state.onBackClick) } + + NewsButtons(buttons = state.buttons, onButtonClick = state.onButtonClick) + } +} + +@Composable +private fun AppBar(onBackClick: () -> Unit) { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResourceSafe(id = R.string.news), + containerColor = TangemTheme.colors.background.primary, + ) +} + +@Suppress("FunctionNaming") +private fun LazyListScope.NewsButtons( + buttons: ImmutableSet, + onButtonClick: (NewsUM.ButtonUM) -> Unit, +) { + items(buttons.toList()) { button -> + PrimaryButton( + text = stringResourceSafe(button.textResId), + onClick = { onButtonClick(button) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt new file mode 100644 index 0000000000..5ed86201e4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.tester.presentation.news.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.feature.tester.presentation.news.state.NewsUM +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +internal class NewsViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(createInitialState()) + val uiState: StateFlow = _uiState + + private var router: InnerTesterRouter? = null + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun createInitialState(): NewsUM { + return NewsUM( + onBackClick = ::onBackClick, + buttons = persistentSetOf( + NewsUM.ButtonUM.NEWS_DETAILS, + ), + onButtonClick = ::onButtonClick, + ) + } + + private fun onBackClick() { + router?.back() + } + + private fun onButtonClick(button: NewsUM.ButtonUM) { + when (button) { + NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index c47c184c25..15b31a07c6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -18,4 +18,6 @@ Share logs Test push Accounts + News + News details diff --git a/settings.gradle.kts b/settings.gradle.kts index 60632a9860..f6e74fac62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:yield-supply:impl") include(":features:feed:api") include(":features:feed:impl") + +include(":features:news:news-details:api") +include(":features:news:news-details:impl") // endregion Feature modules // region Domain modules From 8632680f20956ba0be36c812d50a74c627daa448 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 11:46:50 +0300 Subject: [PATCH 046/130] Updated on 2026-08-14 --- .../ApplicationInjectionExecutionRule.kt | 72 ++++++++++++++++++- .../kotlin/com/tangem/common/BaseTestCase.kt | 27 +++---- .../com/tangem/scenarios/BaseScenarios.kt | 7 +- .../screens/CreateWalletStartPageObject.kt | 23 ++++++ .../com/tangem/screens/DetailsPageObject.kt | 6 ++ .../com/tangem/screens/StoriesPageObject.kt | 14 ++-- .../kotlin/com/tangem/tests/DetailsTest.kt | 11 +-- .../kotlin/com/tangem/tests/FeedbackTest.kt | 14 ++-- .../kotlin/com/tangem/tests/ScanErrorTest.kt | 39 ---------- .../kotlin/com/tangem/tests/StoriesTest.kt | 37 ---------- .../com/tangem/tests/TermsOfServiceTest.kt | 9 +-- .../core/ui/test/DetailsScreenTestTags.kt | 1 + .../features/details/ui/DetailsScreen.kt | 4 +- 13 files changed, 140 insertions(+), 124 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt index 75662e25d4..2445305b72 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -7,23 +7,91 @@ import dagger.hilt.android.testing.OnComponentReadyRunner import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement +import timber.log.Timber -class ApplicationInjectionExecutionRule : TestRule { +class ApplicationInjectionExecutionRule( + private val toggleStates: Map +) : TestRule { private val tangemApplication: TangemApplication get() = ApplicationProvider.getApplicationContext() + private var originalFeatureTogglesValues: Map? = null + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { + saveOriginalFeatureToggles() + overrideFeatureToggles() + OnComponentReadyRunner.addListener( tangemApplication, ApplicationEntryPoint::class.java ) { _: ApplicationEntryPoint -> tangemApplication.preInit() tangemApplication.init() } - base.evaluate() + + try { + base.evaluate() + } finally { + restoreOriginalFeatureToggles() + } } } } + + @Suppress("UNCHECKED_CAST") + private fun saveOriginalFeatureToggles() { + try { + val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") + val valuesField = featureTogglesClass.getDeclaredField("values") + valuesField.isAccessible = true + originalFeatureTogglesValues = valuesField.get(null) as Map + } catch (e: Exception) { + Timber.e("Failed to save original toggles values: ${e.message}") + } + } + + @Suppress("UNCHECKED_CAST") + private fun overrideFeatureToggles() { + try { + val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") + val valuesField = featureTogglesClass.getDeclaredField("values") + valuesField.isAccessible = true + + val originalValues = originalFeatureTogglesValues ?: + (valuesField.get(null) as Map) + + val newValues = originalValues.toMutableMap() + + toggleStates.forEach { (toggle, enabled) -> + if (enabled) { + newValues[toggle] = "1.0.0" + } else { + newValues.remove(toggle) + } + } + + valuesField.set(null, newValues) + + Timber.i("FeatureToggles.values updated: $toggleStates") + + } catch (e: Exception) { + Timber.e("FeatureToggles.values didn't change with error: ${e.message}") + } + } + + private fun restoreOriginalFeatureToggles() { + try { + if (originalFeatureTogglesValues != null) { + val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles") + val valuesField = featureTogglesClass.getDeclaredField("values") + valuesField.isAccessible = true + valuesField.set(null, originalFeatureTogglesValues) + Timber.i("FeatureToggles.values restored") + } + } catch (e: Exception) { + Timber.e("FeatureToggles.values didn't restored with error: ${e.message}") + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 57a2c29563..8a40e56185 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -16,8 +16,6 @@ import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE import com.tangem.common.rules.ApiEnvironmentRule -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -53,9 +51,6 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var appPreferencesStore: AppPreferencesStore - @Inject - lateinit var featureTogglesManager: FeatureTogglesManager - @Inject lateinit var promoRepository: PromoRepository @@ -75,7 +70,7 @@ abstract class BaseTestCase : TestCase( @JvmField val ruleChain: TestRule = RuleChain .outerRule(hiltRule) - .around(ApplicationInjectionExecutionRule()) + .around(applicationInjectionRule()) .around(permissionRule) .around(apiEnvironmentRule) .around(composeTestRule) @@ -110,7 +105,6 @@ abstract class BaseTestCase : TestCase( apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) Intents.init() - setFeatureToggles() additionalBeforeSection() }.after { additionalAfterSection() @@ -141,14 +135,15 @@ abstract class BaseTestCase : TestCase( fun waitForIdle() = composeTestRule.waitForIdle() - private fun setFeatureToggles() { - runBlocking { - with(featureTogglesManager as MutableFeatureTogglesManager) { - changeToggle("NEW_TOKEN_RECEIVE_ENABLED", true) - changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true) - changeToggle("SWAP_REDESIGN_ENABLED", true) - changeToggle("NEW_ONRAMP_MAIN_ENABLED", true) - } - } + private fun applicationInjectionRule(): ApplicationInjectionExecutionRule { + return ApplicationInjectionExecutionRule( + toggleStates = mapOf( + "NEW_TOKEN_RECEIVE_ENABLED" to true, + "WALLET_BALANCE_FETCHER_ENABLED" to true, + "SWAP_REDESIGN_ENABLED" to true, + "NEW_ONRAMP_MAIN_ENABLED" to true, + "HOT_WALLET_ENABLED" to true + ) + ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 1761e307c2..e84ee3a24c 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -26,8 +26,11 @@ fun BaseTestCase.scanCard( step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } } - step("Click on 'Scan' button") { - onStoriesScreen { scanButton.clickWithAssertion() } + step("Click on 'Get started' button") { + onStoriesScreen { getStartedButton.clickWithAssertion() } + } + step("Click on 'Scan card or ring' button") { + onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } } if (alreadyActivatedDialogIsShown) { step("Click on 'This is my wallet' button") { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt new file mode 100644 index 0000000000..b06335d752 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt @@ -0,0 +1,23 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.onboarding.v2.impl.R as OnboardingImplR + +class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val scanCardOrRingButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 62b73d6c4b..49cc06a3ad 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -42,6 +42,7 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasText(getResourceString(R.string.app_settings_title)) } + val contactSupportButton: KNode = child { hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasText(getResourceString(R.string.details_row_title_contact_to_support)) @@ -50,6 +51,11 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasText(getResourceString(R.string.disclaimer_title)) } + + val versionName: KNode = child { + hasTestTag(DetailsScreenTestTags.VERSION_NAME) + useUnmergedTree = true + } } internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt index f9666ceeb4..faf2492a94 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt @@ -2,20 +2,20 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase -import com.tangem.core.ui.test.StoriesScreenTestTags +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.features.onboarding.v2.impl.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString class StoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { - val scanButton: KNode = child { - hasTestTag(StoriesScreenTestTags.SCAN_BUTTON) - } - - val orderButton: KNode = child { - hasTestTag(StoriesScreenTestTags.ORDER_BUTTON) + val getStartedButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_get_started)) + useUnmergedTree = true } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 49fe556eba..c084d381c8 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -4,10 +4,7 @@ import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.scenarios.openMainScreen -import com.tangem.screens.onDetailsScreen -import com.tangem.screens.onReferralProgramScreen -import com.tangem.screens.onTopBar -import com.tangem.screens.onWalletSettingsScreen +import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -31,9 +28,6 @@ class DetailsTest : BaseTestCase() { step("Assert 'Wallet connect' button is visible") { walletConnectButton.assertIsDisplayed() } - step("Assert 'Scan card' button is visible") { - scanCardButton.assertIsDisplayed() - } step("Assert 'Buy Tangem card' button is visible") { buyTangemButton.assertIsDisplayed() } @@ -131,9 +125,6 @@ class DetailsTest : BaseTestCase() { step("Assert 'Wallet connect' button does not exist") { walletConnectButton.assertIsNotDisplayed() } - step("Assert 'Scan card' button is visible") { - scanCardButton.assertIsDisplayed() - } step("Assert 'Buy Tangem card' button is visible") { buyTangemButton.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 9d75140152..0a20f32754 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -139,8 +139,11 @@ class FeedbackTest : BaseTestCase() { step("Set scanning error") { MockProvider.setEmulateError(TangemSdkError.TagLost()) } - step("Click on 'Scan' button") { - onStoriesScreen { scanButton.performClick() } + step("Click on 'Get started' button") { + onStoriesScreen { getStartedButton.clickWithAssertion() } + } + step("Click on 'Scan card or ring' button") { + onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } } step("Force show 'Scan warning' dialog"){ runOnUiThread { @@ -178,8 +181,11 @@ class FeedbackTest : BaseTestCase() { step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } } - step("Click on 'Scan' button") { - onStoriesScreen { scanButton.clickWithAssertion() } + step("Click on 'Get started' button") { + onStoriesScreen { getStartedButton.clickWithAssertion() } + } + step("Click on 'Scan card or ring' button") { + onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } } step("Check 'Already used Wallet' dialog") { checkAlreadyUsedWalletDialog() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt deleted file mode 100644 index c31340f9dd..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tests - -import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.clickWithAssertion -import com.tangem.screens.onDisclaimerScreen -import com.tangem.screens.onMainScreen -import com.tangem.screens.onStoriesScreen -import com.tangem.tap.domain.sdk.mocks.MockProvider -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class ScanErrorTest : BaseTestCase() { - - @Test - fun goToMainTest() = - setupHooks().run { - onDisclaimerScreen { - step("Click on 'Accept' button") { - acceptButton.clickWithAssertion() - } - } - onStoriesScreen { - step("Click on 'Scan' button emulating scan error") { - MockProvider.setEmulateError() - scanButton.clickWithAssertion() - } - step("Click on 'Scan' button again without emulating error") { - MockProvider.resetEmulateError() - scanButton.clickWithAssertion() - } - } - onMainScreen { - step("Make sure wallet screen is visible") { - assertIsDisplayed() - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt deleted file mode 100644 index d87e1be98c..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.tests - -import android.content.Intent.ACTION_VIEW -import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.clickWithAssertion -import com.tangem.screens.onDisclaimerScreen -import com.tangem.screens.onStoriesScreen -import dagger.hilt.android.testing.HiltAndroidTest -import io.github.kakaocup.kakao.intent.KIntent - -@HiltAndroidTest -class StoriesTest : BaseTestCase() { - - // @Test - fun clickOnOrderButtonTest() = - setupHooks().run { - val buyWalletUrl = "https://buy.tangem.com/" - onDisclaimerScreen { - step("Click on 'Accept' button") { - acceptButton.clickWithAssertion() - } - } - onStoriesScreen { - step("Click on 'Order' button") { - orderButton.clickWithAssertion() - } - step("Assert: browser opened") { - val expectedIntent = KIntent { - hasAction(ACTION_VIEW) - hasData { toString().startsWith(buyWalletUrl) } - } - expectedIntent.intended() - device.uiDevice.pressBack() - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt index 3b956d2bea..046964ff53 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt @@ -37,8 +37,7 @@ class TermsOfServiceTest : BaseTestCase() { } step("Assert 'Stories' screen is opened") { onStoriesScreen { - scanButton.assertIsDisplayed() - orderButton.assertIsDisplayed() + getStartedButton.assertIsDisplayed() } } } @@ -91,8 +90,7 @@ class TermsOfServiceTest : BaseTestCase() { } step("Assert 'Stories' screen is opened") { onStoriesScreen { - scanButton.assertIsDisplayed() - orderButton.assertIsDisplayed() + getStartedButton.assertIsDisplayed() } } step("Stop app") { @@ -103,8 +101,7 @@ class TermsOfServiceTest : BaseTestCase() { } step("Assert 'Stories' screen is opened") { onStoriesScreen { - scanButton.assertIsDisplayed() - orderButton.assertIsDisplayed() + getStartedButton.assertIsDisplayed() } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt index 33f7b56ef3..bbbeb91dd5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt @@ -3,4 +3,5 @@ package com.tangem.core.ui.test object DetailsScreenTestTags { const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER" const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM" + const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME" } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 0bd950c5c8..6be90f525b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -185,7 +185,9 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { } Text( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing6) + .testTag(DetailsScreenTestTags.VERSION_NAME), text = model.appVersion, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, From e024d739b72ea81f3d88c2e750b0d4869617ab69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 14:17:15 +0500 Subject: [PATCH 047/130] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 2 +- .../tap/common/analytics/events/Push.kt | 2 +- .../tap/common/analytics/events/Settings.kt | 4 +- .../details/redux/DetailsMiddleware.kt | 2 +- .../AppSettingsItemsAnalyticsSender.kt | 2 +- .../ui/dialogs/UnfinishedBackupFoundDialog.kt | 4 +- .../models/event/MainScreenAnalyticsEvent.kt | 16 ++--- .../pair/DefaultWcPairUseCase.kt | 2 +- .../card/analytics/IntroductionProcess.kt | 8 +-- .../com/tangem/domain/card/analytics/Shop.kt | 2 +- .../domain/nft/analytics/NFTAnalyticsEvent.kt | 10 +-- .../onramp/analytics/OnrampAnalyticsEvent.kt | 20 +++--- .../analytics/StakingAnalyticsEvent.kt | 28 ++++---- .../model/analytics/PromoAnalyticsEvent.kt | 10 +-- .../analytics/TokenOnrampAnalyticsEvent.kt | 2 +- .../tangempay/TangemPayAnalyticsEvents.kt | 16 ++--- .../domain/walletconnect/WcAnalyticEvents.kt | 22 +++---- .../usecase/disconnect/WcDisconnectUseCase.kt | 2 +- .../domain/wallets/analytics/Settings.kt | 4 +- .../features/home/impl/model/HomeModel.kt | 10 +-- .../tangem/features/kyc/DefaultKycModel.kt | 2 +- .../analytics/ManageTokensAnalyticEvent.kt | 2 +- .../model/OnboardingManageTokensModel.kt | 2 +- .../analytics/MarketsListAnalyticsEvent.kt | 10 +-- .../tokenlist/impl/model/MarketsListModel.kt | 14 ++-- .../nft/details/model/NFTDetailsModel.kt | 10 +-- .../nft/receive/model/NFTReceiveModel.kt | 2 +- .../v2/common/analytics/OnboardingEvent.kt | 50 +++++--------- .../entry/impl/model/OnboardingEntryModel.kt | 8 +-- .../DefaultOnboardingMultiWalletComponent.kt | 2 +- .../model/MultiWalletAccessCodeModel.kt | 6 +- .../backup/model/MultiWalletBackupModel.kt | 8 +-- .../model/Wallet1ChooseOptionModel.kt | 4 +- .../model/MultiWalletCreateWalletModel.kt | 4 +- .../impl/model/OnboardingMultiWalletModel.kt | 2 +- .../model/OnboardingNoteCreateWalletModel.kt | 4 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 4 +- .../model/OnboardingVisaAccessCodeModel.kt | 8 +-- .../model/OnboardingVisaApproveModel.kt | 4 +- .../model/OnboardingVisaInProgressModel.kt | 2 +- .../model/OnboardingVisaOtherWalletModel.kt | 6 +- .../model/OnboardingVisaPinCodeModel.kt | 10 +-- .../model/OnboardingVisaWelcomeModel.kt | 4 +- .../analytics/OnboardingVisaAnalyticsEvent.kt | 34 +++++----- .../alloffers/entity/AllOffersStateFactory.kt | 2 +- .../onramp/alloffers/model/AllOffersModel.kt | 4 +- .../model/ConfirmResidencyModel.kt | 4 +- .../amount/OnrampAmountStateFactory.kt | 4 +- .../main/model/OnrampMainComponentModel.kt | 4 +- .../factory/OnrampV2AmountStateFactory.kt | 4 +- .../model/OnrampV2MainComponentModel.kt | 4 +- .../providers/model/SelectProviderModel.kt | 4 +- .../model/OnrampSelectCountryModel.kt | 2 +- .../selecttoken/model/OnrampOperationModel.kt | 4 +- .../settings/model/OnrampSettingsModel.kt | 2 +- .../model/OnrampSuccessComponentModel.kt | 2 +- .../swap/DefaultSwapSelectTokensComponent.kt | 2 +- .../referral/analytics/ReferralEvents.kt | 12 ++-- .../feature/referral/model/ReferralModel.kt | 16 ++--- .../impl/presentation/model/StakingModel.kt | 10 +-- .../presentation/state/StakingStateRouter.kt | 4 +- .../feature/swap/analytics/SwapEvents.kt | 12 ++-- .../tangem/feature/swap/model/SwapModel.kt | 12 ++-- .../tangempay/model/TangemPayDetailsModel.kt | 4 +- .../model/TangemPayOnboardingModel.kt | 6 +- .../tokendetails/model/TokenDetailsModel.kt | 4 +- ...nDetailsOnrampTransactionStateConverter.kt | 10 +-- .../model/WalletSettingsModel.kt | 2 +- .../model/OrganizeTokensModel.kt | 12 ++-- .../wallet/child/wallet/model/WalletModel.kt | 2 +- .../wallet/model/intents/VisaWalletIntents.kt | 4 +- .../model/intents/WalletCardClickIntents.kt | 4 +- .../intents/WalletWarningsClickIntents.kt | 42 ++++++------ .../deeplink/DefaultPromoDeeplinkHandler.kt | 2 +- .../analytics/PromoActivationAnalytics.kt | 2 +- .../PortfolioOrganizeTokensAnalyticsEvent.kt | 8 +-- .../analytics/WalletScreenAnalyticsEvent.kt | 66 +++++++++---------- .../utils/SelectedWalletAnalyticsSender.kt | 2 +- .../WalletTangemPayAnalyticsEventSender.kt | 4 +- .../utils/WalletWarningsAnalyticsSender.kt | 30 ++++----- .../transformers/SetVisaInfoTransformer.kt | 2 +- .../BalancesAndLimitsBottomSheetConverter.kt | 4 +- .../SingleWalletOnrampTransactionConverter.kt | 6 +- .../DefaultPromoDeeplinkHandlerTest.kt | 60 ++++++++++++----- .../connections/model/WcConnectionsModel.kt | 2 +- .../api/analytics/YieldSupplyAnalytics.kt | 2 +- .../impl/promo/model/YieldSupplyPromoModel.kt | 2 +- 87 files changed, 386 insertions(+), 374 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8db7434ef7..076a21b682 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -367,7 +367,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true if (isFromPush) { - analyticsEventsHandler.send(Push.PushNotificationOpened) + analyticsEventsHandler.send(Push.PushNotificationOpened()) } handleDeepLink(intent = intent, isFromOnNewIntent = true) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt index c4345c597f..acdec4105e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt @@ -8,5 +8,5 @@ internal sealed class Push(event: String) : AnalyticsEvent( params = emptyMap(), ) { - data object PushNotificationOpened : Push(event = "Push Notification Opened") + class PushNotificationOpened : Push(event = "Push Notification Opened") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 3ac65172d2..c303a9cbba 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -67,7 +67,7 @@ sealed class Settings( params = mapOf("State" to state.value), ) - object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication") + class ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication") class MainCurrencyChanged(currencyType: String) : AppSettings( event = "Main Currency Changed", @@ -79,7 +79,7 @@ sealed class Settings( params = mapOf("State" to theme.value), ) - object EnableBiometrics : AppSettings(event = "Notice - Enable Biometric") + class EnableBiometrics : AppSettings(event = "Notice - Enable Biometric") class HideBalanceChanged(state: AnalyticsParam.OnOffState) : AppSettings( event = "Hide Balance Changed", diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3a4901f8e3..e3e1923c6b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -196,7 +196,7 @@ class DetailsMiddleware { } private fun enrollBiometrics() { - Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) + Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication()) store.inject(DaggerGraphState::settingsManager).openBiometricSettings() } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt index 64a5832a8c..520610a058 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt @@ -25,7 +25,7 @@ internal class AppSettingsItemsAnalyticsSender @Inject constructor( private fun getEvent(item: AppSettingsScreenState.Item): AnalyticsEvent? { return when (item.id) { - AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics + AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics() else -> null } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index fbeff5f6f4..df8cc61880 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -18,12 +18,12 @@ object UnfinishedBackupFoundDialog { setTitle(R.string.common_warning) setMessage(R.string.welcome_interrupted_backup_alert_message) setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> - Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup) + Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup()) store.dispatch(GlobalAction.HideDialog) store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse)) } setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> - Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup) + Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup()) store.dispatch(GlobalAction.HideDialog) store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse))) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index c8864d3617..a1a2ef6911 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -36,23 +36,23 @@ sealed class MainScreenAnalyticsEvent( }, ) - data object ButtonReceive : MainScreenAnalyticsEvent( + class ButtonReceive : MainScreenAnalyticsEvent( event = "Button - Receive", ) - data object LimitsClicked : MainScreenAnalyticsEvent( + class LimitsClicked : MainScreenAnalyticsEvent( event = "Limits Clicked", ) - data object NoticeBalancesInfo : MainScreenAnalyticsEvent( + class NoticeBalancesInfo : MainScreenAnalyticsEvent( event = "Notice - Balances Info", ) - data object NoticeLimitsInfo : MainScreenAnalyticsEvent( + class NoticeLimitsInfo : MainScreenAnalyticsEvent( event = "Notice - Limits Info", ) - data object ButtonExplore : MainScreenAnalyticsEvent( + class ButtonExplore : MainScreenAnalyticsEvent( event = "Button - Explore", ) @@ -66,11 +66,11 @@ sealed class MainScreenAnalyticsEvent( params = mapOf(AnalyticsParam.STATUS to status.value), ) - data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened") + class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened") - data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened") + class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened") - data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened") + class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened") data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( event = "Buy Token Clicked", diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index d060e85384..6e6396e89a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -166,7 +166,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } override fun approve(sessionForApprove: WcSessionApprove) { - analytics.send(WcAnalyticEvents.PairButtonConnect) + analytics.send(WcAnalyticEvents.PairButtonConnect()) onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) } 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 06b18ef403..b6ec1affa6 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 @@ -8,10 +8,10 @@ sealed class IntroductionProcess( params: Map = emptyMap(), ) : AnalyticsEvent("Introduction Process", event, params) { - object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") - object ButtonTokensList : IntroductionProcess("Button - Tokens List") - object ButtonBuyCards : IntroductionProcess("Button - Buy Cards") - object ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") + class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") + class ButtonTokensList : IntroductionProcess("Button - Tokens List") + class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") + class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") class ButtonScanCard( val source: AnalyticsParam.ScreensSources, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 11536725f8..37ae7f2702 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -7,5 +7,5 @@ sealed class Shop( params: Map = emptyMap(), ) : AnalyticsEvent("Shop", event, params) { - object ScreenOpened : Shop("Shop Screen Opened") + class ScreenOpened : Shop("Shop Screen Opened") } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt index 0c0e7d83cd..ade188cf05 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt @@ -34,7 +34,7 @@ sealed class NFTAnalyticsEvent( ) object Receive { - data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") + class ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") data class BlockchainChosen( private val blockchain: String, @@ -63,9 +63,9 @@ sealed class NFTAnalyticsEvent( }, ) - data object ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More") - data object ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All") - data object ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore") - data object ButtonSend : NFTAnalyticsEvent(event = "Button - Send") + class ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More") + class ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All") + class ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore") + class ButtonSend : NFTAnalyticsEvent(event = "Button - Send") } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index b7da9150d6..d9d98eac03 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -30,7 +30,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened") + class SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened") data class FiatCurrencyChosen( private val currency: String, @@ -39,11 +39,11 @@ sealed class OnrampAnalyticsEvent( params = mapOf("Currency Type" to currency), ) - data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close") + class CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close") - data object SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened") + class SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened") - data object SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened") + class SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened") data class OnResidenceChosen( private val residence: String, @@ -59,7 +59,7 @@ sealed class OnrampAnalyticsEvent( params = mapOf(RESIDENCE to residence), ) - data object OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change") + class OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change") data class OnResidenceConfirm( private val residence: String, @@ -68,7 +68,7 @@ sealed class OnrampAnalyticsEvent( params = mapOf(RESIDENCE to residence), ) - data object ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened") + class ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened") data class ProviderCalculated( private val providerName: String, @@ -83,7 +83,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened") + class PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened") data class OnPaymentMethodChosen( private val paymentMethod: String, @@ -135,8 +135,8 @@ sealed class OnrampAnalyticsEvent( ), ) - data object MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount") - data object MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount") + class MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount") + class MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount") data class Errors( private val tokenSymbol: String, @@ -203,7 +203,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object AllOffersClicked : OnrampAnalyticsEvent( + class AllOffersClicked : OnrampAnalyticsEvent( event = "Button - All Offers", params = emptyMap(), ) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index 42da91d04f..e2950b602d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -2,7 +2,6 @@ package com.tangem.domain.staking.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.models.staking.action.StakingActionType @@ -24,11 +23,11 @@ sealed class StakingAnalyticsEvent( ), ) - data object WhatIsStaking : StakingAnalyticsEvent( + class WhatIsStaking : StakingAnalyticsEvent( event = "Link - What Is Staking", ) - data object AmountScreenOpened : StakingAnalyticsEvent( + class AmountScreenOpened : StakingAnalyticsEvent( event = "Amount Screen Opened", ) @@ -54,7 +53,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object RewardScreenOpened : StakingAnalyticsEvent( + class RewardScreenOpened : StakingAnalyticsEvent( event = "Reward Screen Opened", ) @@ -67,7 +66,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonMax : StakingAnalyticsEvent( + class ButtonMax : StakingAnalyticsEvent( event = "Button - Max", ) @@ -98,7 +97,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonRewards : StakingAnalyticsEvent( + class ButtonRewards : StakingAnalyticsEvent( event = "Button - Rewards", ) @@ -112,9 +111,9 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonShare : StakingAnalyticsEvent(event = "Button - Share") + class ButtonShare : StakingAnalyticsEvent(event = "Button - Share") - data object ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore") + class ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore") data class StakeKitApiError( val stakingError: StakingError.StakeKitApiError, @@ -145,12 +144,6 @@ sealed class StakingAnalyticsEvent( }, ) - fun MutableMap.addIfValueIsNotNull(key: String, value: Any?) { - if (value != null) { - put(key, value.toString()) - } - } - data class TransactionError( val errorCode: String, ) : StakingAnalyticsEvent( @@ -178,4 +171,11 @@ sealed class StakingAnalyticsEvent( enum class StakeScreenSource { Info, Amount, Confirmation, Validators, +} + +@Suppress("CanBeNonNullable") +fun MutableMap.addIfValueIsNotNull(key: String, value: Any?) { + if (value != null) { + put(key, value.toString()) + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt index e0c863bf61..dcacf45e9e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt @@ -31,22 +31,22 @@ sealed class PromoAnalyticsEvent( ), ) { sealed class BannerAction(val action: String) { - data object Clicked : BannerAction(action = "Clicked") - data object Closed : BannerAction(action = "Closed") + class Clicked : BannerAction(action = "Clicked") + class Closed : BannerAction(action = "Closed") } } // region visa waitlist promo - data object VisaWaitlistPromo : PromoAnalyticsEvent(event = "Visa Waitlist") + class VisaWaitlistPromo : PromoAnalyticsEvent(event = "Visa Waitlist") - data object VisaWaitlistPromoJoin : PromoAnalyticsEvent( + class VisaWaitlistPromoJoin : PromoAnalyticsEvent( event = "Button - Join Now", params = mapOf( "Program Name" to "Visa Waitlist", ), ) - data object VisaWaitlistPromoDismiss : PromoAnalyticsEvent( + class VisaWaitlistPromoDismiss : PromoAnalyticsEvent( event = "Button - Close", params = mapOf( "Program Name" to "Visa Waitlist", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index ffa78172b9..8261c75b29 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -30,7 +30,7 @@ sealed class TokenOnrampAnalyticsEvent( ), ) - data object GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider") + class GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider") data class NoticeKYC( val tokenSymbol: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index af92401e5b..50240853c7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -8,42 +8,42 @@ sealed class TangemPayAnalyticsEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = categoryName, event = event, params = params) { - data object ActivationScreenOpened : TangemPayAnalyticsEvents( + class ActivationScreenOpened : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Activation Screen Opened", ) - data object ViewTermsClicked : TangemPayAnalyticsEvents( + class ViewTermsClicked : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Button - Visa View Terms", ) - data object GetCardClicked : TangemPayAnalyticsEvents( + class GetCardClicked : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Button - Visa Get Card", ) - data object KycFlowOpened : TangemPayAnalyticsEvents( + class KycFlowOpened : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa KYC Flow Opened", ) - data object IssuingBannerDisplayed : TangemPayAnalyticsEvents( + class IssuingBannerDisplayed : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa Issuing Banner Displayed", ) - data object MainScreenOpened : TangemPayAnalyticsEvents( + class MainScreenOpened : TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Visa Main Screen Opened", ) - data object ReceiveFundsClicked : TangemPayAnalyticsEvents( + class ReceiveFundsClicked : TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Button - Visa Receive", ) - data object SwapClicked : TangemPayAnalyticsEvents( + class SwapClicked : TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Button - Visa Swap", ) diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 8bcf3eb116..6a0ac90cb8 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -5,7 +5,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.models.network.Network -import com.tangem.domain.walletconnect.WcAnalyticEvents.ButtonDisconnectAll.toAnalyticVerificationStatus +import com.tangem.domain.walletconnect.WcAnalyticEvents.DAppVerificationStatus import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -19,7 +19,7 @@ sealed class WcAnalyticEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = WC_CATEGORY_NAME, event = event, params = params) { - object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") + class ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( event = "Session Initiated", params = mapOf( @@ -32,7 +32,7 @@ sealed class WcAnalyticEvents( ), ) - data object PairButtonConnect : WcAnalyticEvents( + class PairButtonConnect : WcAnalyticEvents( event = "Button - Connect", ) @@ -203,7 +203,7 @@ sealed class WcAnalyticEvents( } } - data object ButtonDisconnectAll : WcAnalyticEvents( + class ButtonDisconnectAll : WcAnalyticEvents( event = "Button - Disconnect All", ) @@ -265,16 +265,16 @@ sealed class WcAnalyticEvents( Unknown("Unknown"), } - fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { - SAFE -> DAppVerificationStatus.Verified - UNSAFE -> DAppVerificationStatus.Risky - FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown - }.status - companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" const val WC_CATEGORY_NAME = "Wallet Connect" } -} \ No newline at end of file +} + +fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { + SAFE -> DAppVerificationStatus.Verified + UNSAFE -> DAppVerificationStatus.Risky + FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown +}.status \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt index cb6c3a2497..8c73940afe 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -15,7 +15,7 @@ class WcDisconnectUseCase( ) { suspend fun disconnectAll() { - analytics.send(WcAnalyticEvents.ButtonDisconnectAll) + analytics.send(WcAnalyticEvents.ButtonDisconnectAll()) sessionsManager.sessions.first() .flatMap { it.value } .map { session -> flow { emit(internalDisconnect(session)) } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt index 6bc640e1e2..703dcb8cbb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt @@ -8,7 +8,5 @@ sealed class Settings( params: Map = emptyMap(), ) : AnalyticsEvent(category, event, params) { - data object ButtonCreateBackup : Settings(event = "Button - Create Backup") - - data object ButtonManageTokens : Settings(event = "Button - Manage Tokens") + class ButtonManageTokens : Settings(event = "Button - Manage Tokens") } \ No newline at end of file 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 a02c3c4543..1dbc4c7a1f 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 @@ -93,7 +93,7 @@ internal class HomeModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - analyticsEventHandler.send(IntroductionProcess.ScreenOpened) + analyticsEventHandler.send(IntroductionProcess.ScreenOpened()) observeUserCountryChanges() when (params.launchMode) { @@ -127,20 +127,20 @@ internal class HomeModel @Inject constructor( } private fun onScanClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy) + analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy()) scanCard() } private fun onShopClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) - analyticsEventHandler.send(Shop.ScreenOpened) + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) + analyticsEventHandler.send(Shop.ScreenOpened()) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onSearchTokensClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonTokensList) + analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) router.push(AppRoute.ManageTokens(Source.STORIES)) } diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt index 2d7b7e1558..ba0446ea8a 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -30,7 +30,7 @@ class DefaultKycModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - analytics.send(TangemPayAnalyticsEvents.KycFlowOpened) + analytics.send(TangemPayAnalyticsEvents.KycFlowOpened()) modelScope.launch { try { kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt index 4ef52f5c05..01f9d2a5d0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt @@ -49,7 +49,7 @@ internal sealed class ManageTokensAnalyticEvent( AnalyticsParam.Key.SOURCE to source.analyticsName, ), ) - data object ButtonLater : ManageTokensAnalyticEvent( + class ButtonLater : ManageTokensAnalyticEvent( event = "Button - Later", params = mapOf(AnalyticsParam.SOURCE to ManageTokensSource.ONBOARDING.analyticsName), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index ae218b4341..50267c156a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -287,7 +287,7 @@ internal class OnboardingManageTokensModel @Inject constructor( } }, ) { - analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater) + analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater()) useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt index f1916f48bb..c5f92d0700 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -9,7 +9,7 @@ internal sealed class MarketsListAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent(category = "Markets", event = event, params = params) { - data object BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened") + class BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened") data class SortBy( val sortByTypeUM: SortByTypeUM, @@ -33,11 +33,11 @@ internal sealed class MarketsListAnalyticsEvent( ), ) - data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") - data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") + class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") - data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( event = "Token Searched", @@ -46,5 +46,5 @@ internal sealed class MarketsListAnalyticsEvent( ), ) - data object ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens") + class ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens") } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 2018d1eb53..22574f445e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -69,9 +69,9 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, - onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, + onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) }, onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, - onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) }, + onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, ) private val mainMarketsListManager = MarketsListBatchFlowManager( @@ -150,7 +150,7 @@ internal class MarketsListModel @Inject constructor( if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && stakingNotificationMaxApy != null ) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown()) } marketsListUMStateManager.onUiItemsChanged( @@ -267,9 +267,9 @@ internal class MarketsListModel @Inject constructor( } private fun initAnalytics() { - containerBottomSheetState.onEach { - if (it == BottomSheetState.EXPANDED) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + containerBottomSheetState.onEach { bottomSheetState -> + if (bottomSheetState == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened()) } }.launchIn(modelScope) @@ -303,7 +303,7 @@ internal class MarketsListModel @Inject constructor( } private fun onStakingNotificationCloseClick() { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed()) modelScope.launch { promoRepository.setMarketsStakingNotificationHideClicked() } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 966c793b66..e47a5fca31 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -69,7 +69,7 @@ internal class NFTDetailsModel @Inject constructor( onBackClick = { params.onBackClick() }, onReadMoreClick = ::onReadMoreClick, onSeeAllTraitsClick = { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll()) params.onAllTraitsClick() }, onExploreClick = ::onExploreClick, @@ -161,7 +161,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onInfoBlockClick(title: TextReference, text: TextReference) { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore()) bottomSheetNavigation.activate( NFTDetailsBottomSheetConfig.Info( title = title, @@ -171,7 +171,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onReadMoreClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore()) when (val topInfo = _state.value.nftAsset.topInfo) { is NFTAssetUM.TopInfo.Empty -> Unit is NFTAssetUM.TopInfo.Content -> { @@ -186,7 +186,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onExploreClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore()) modelScope.launch { val url = getNFTExploreUrlUseCase.invoke( network = params.nftAsset.network, @@ -199,7 +199,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onSendClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSend) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSend()) router.push( AppRoute.NFTSend( userWalletId = params.userWalletId, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index e9ebb194f0..889afd28d6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -89,7 +89,7 @@ internal class NFTReceiveModel @Inject constructor( val state: StateFlow get() = _state init { - analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened) + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened()) subscribeToNFTAvailableNetworks() loadPortfolioName() } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 33016dc2eb..2050039cc6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.models.currency.CryptoCurrency sealed class OnboardingEvent( category: String, @@ -10,16 +9,16 @@ sealed class OnboardingEvent( params: Map = mapOf(), ) : AnalyticsEvent(category, event, params) { - data object Started : OnboardingEvent("Onboarding", "Onboarding Started") - data object Finished : OnboardingEvent("Onboarding", "Onboarding Finished") + class Started : OnboardingEvent("Onboarding", "Onboarding Started") + class Finished : OnboardingEvent("Onboarding", "Onboarding Finished") sealed class CreateWallet( event: String, params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Create Wallet", event, params) { - data object ScreenOpened : CreateWallet("Create Wallet Screen Opened") - data object ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class ScreenOpened : CreateWallet("Create Wallet Screen Opened") + class ButtonCreateWallet : CreateWallet("Button - Create Wallet") class WalletCreatedSuccessfully( creationType: WalletCreationType = WalletCreationType.PrivateKey, seedPhraseLength: Int? = null, @@ -40,54 +39,39 @@ sealed class OnboardingEvent( } } - sealed class Topup( - event: String, - params: Map = mapOf(), - ) : OnboardingEvent("Onboarding / Top Up", event, params) { - - object ScreenOpened : Topup("Activation Screen Opened") - - object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") - - class ButtonBuyCrypto(currency: CryptoCurrency) : Topup( - event = "Button - Buy Crypto", - params = mapOf(AnalyticsParam.CURRENCY to currency.symbol), - ) - } - sealed class Backup( event: String, params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Backup", event, params) { - data object ScreenOpened : Backup("Backup Screen Opened") - data object Started : Backup("Backup Started") - data object Skipped : Backup("Backup Skipped") - data object SettingAccessCodeStarted : Backup("Setting Access Code Started") - data object AccessCodeEntered : Backup("Access Code Entered") - data object AccessCodeReEntered : Backup("Access Code Re-entered") + class ScreenOpened : Backup("Backup Screen Opened") + class Started : Backup("Backup Started") + class Skipped : Backup("Backup Skipped") + class SettingAccessCodeStarted : Backup("Setting Access Code Started") + class AccessCodeEntered : Backup("Access Code Entered") + class AccessCodeReEntered : Backup("Access Code Re-entered") class Finished(cardsCount: Int) : Backup( event = "Backup Finished", params = mapOf("Cards count" to "$cardsCount"), ) - data object ResetCancelEvent : Backup( + class ResetCancelEvent : Backup( event = "Reset Card Notification", params = mapOf("Option" to "Cancel"), ) - data object ResetPerformEvent : Backup( + class ResetPerformEvent : Backup( event = "Reset Card Notification", params = mapOf("Option" to "Reset"), ) - data object ResumeInterruptedBackup : Backup( + class ResumeInterruptedBackup : Backup( event = "Notice - Backup Canceled", params = mapOf("Action" to "Resume"), ) - data object CancelInterruptedBackup : Backup( + class CancelInterruptedBackup : Backup( event = "Notice - Backup Canceled", params = mapOf("Action" to "Cancel"), ) @@ -98,9 +82,9 @@ sealed class OnboardingEvent( params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Twins", event, params) { - data object ScreenOpened : Twins("Twinning Screen Opened") - data object SetupStarted : Twins("Twin Setup Started") - data object SetupFinished : Twins("Twin Setup Finished") + class ScreenOpened : Twins("Twinning Screen Opened") + class SetupStarted : Twins("Twin Setup Started") + class SetupFinished : Twins("Twin Setup Finished") } data class OfflineAttestationFailed( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 0827c50a61..6f3f8945e9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -157,14 +157,14 @@ internal class OnboardingEntryModel @Inject constructor( modelScope.launch { if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) { doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)), ) } else { doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( @@ -182,7 +182,7 @@ internal class OnboardingEntryModel @Inject constructor( override fun onAllowed() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On)) doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( @@ -195,7 +195,7 @@ internal class OnboardingEntryModel @Inject constructor( override fun onDenied() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off)) doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index f13142d4d5..6526071a17 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -209,7 +209,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor } Done -> { // final step - navigate to parent - analyticsHandler.send(OnboardingEvent.Finished) + analyticsHandler.send(OnboardingEvent.Finished()) val userWallet = childParams.multiWalletState.value.resultUserWallet ?: return params.onDone(userWallet) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt index a432deb8d0..cc9e104b95 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt @@ -37,7 +37,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( val onDismiss = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.Backup.SettingAccessCodeStarted) + analyticsHandler.send(OnboardingEvent.Backup.SettingAccessCodeStarted()) params.multiWalletState.update { it.copy(accessCode = null) @@ -112,7 +112,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( } } - analyticsHandler.send(OnboardingEvent.Backup.AccessCodeEntered) + analyticsHandler.send(OnboardingEvent.Backup.AccessCodeEntered()) } MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> { if (checkAccessCode()) { @@ -125,7 +125,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( modelScope.launch { onDismiss.emit(Unit) } } - analyticsHandler.send(OnboardingEvent.Backup.AccessCodeReEntered) + analyticsHandler.send(OnboardingEvent.Backup.AccessCodeReEntered()) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 609e1e7b6d..d070a4ffed 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -80,10 +80,10 @@ class MultiWalletBackupModel @Inject constructor( init { // for wallet 1 this event is sent in Wallet1ChooseOptionModel if (scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring) { - analyticsEventHandler.send(OnboardingEvent.Backup.ScreenOpened) + analyticsEventHandler.send(OnboardingEvent.Backup.ScreenOpened()) } - analyticsEventHandler.send(OnboardingEvent.Backup.Started) + analyticsEventHandler.send(OnboardingEvent.Backup.Started()) // Clear any saved backup before starting the backup process // also clears the primary card if it was set @@ -226,7 +226,7 @@ class MultiWalletBackupModel @Inject constructor( _uiState.update { it.copy(dialog = null) } }, onDismissClick = { - analyticsEventHandler.send(OnboardingEvent.Backup.ResetCancelEvent) + analyticsEventHandler.send(OnboardingEvent.Backup.ResetCancelEvent()) }, ), ) @@ -270,7 +270,7 @@ class MultiWalletBackupModel @Inject constructor( } private fun resetBackupCard(cardId: String) { - analyticsEventHandler.send(OnboardingEvent.Backup.ResetPerformEvent) + analyticsEventHandler.send(OnboardingEvent.Backup.ResetPerformEvent()) modelScope.launch { tangemSdkManager.resetToFactorySettings( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index e17f81fcdb..8d508c9a4b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -35,7 +35,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( val returnToParentFlow = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened) + analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened()) } val canSkipBackup = params.multiWalletState.value.currentScanResponse.card.canSkipBackup @@ -44,7 +44,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( if (skipClicked) return skipClicked = true - analyticsHandler.send(OnboardingEvent.Backup.Skipped) + analyticsHandler.send(OnboardingEvent.Backup.Skipped()) modelScope.launch { val scanResponse = params.multiWalletState.value.currentScanResponse diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 1567ffe625..45bb77cd1c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -63,7 +63,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( resourceReference(R.string.onboarding_create_wallet_body) }, onCreateWalletClick = { - analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonCreateWallet) + analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonCreateWallet()) createWallet(false) }, showOtherOptionsButton = params.parentParams.withSeedPhraseFlow, @@ -80,7 +80,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened) + analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened()) } private fun createWallet(shouldReset: Boolean) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 9895662a0a..d254fca350 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -58,7 +58,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - analyticsHandler.send(OnboardingEvent.Started) + analyticsHandler.send(OnboardingEvent.Started()) initScreenTitle() loadCardArtwork() subscribeToBackups() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index efecb19956..50850543c5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -41,11 +41,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( ) init { - Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened) + Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened()) modelScope.launch { val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) { - Analytics.send(OnboardingEvent.Started) + Analytics.send(OnboardingEvent.Started()) } } observeArtwork() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index f1559c092d..efa215b517 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -103,7 +103,7 @@ internal class OnboardingTwinModel @Inject constructor( init { when (_uiState.value) { is OnboardingTwinUM.Welcome -> { - analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened()) modelScope.launch { saveTwinsOnboardingShownUseCase() } @@ -272,7 +272,7 @@ internal class OnboardingTwinModel @Inject constructor( } private fun scanComplete(scanResponse: ScanResponse) { - analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished) + analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished()) when (params.mode) { Mode.WelcomeOnly -> saveWalletAndDone() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index b921e0641d..2d81af1b09 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -71,7 +71,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted()) } fun onBack() { @@ -115,14 +115,14 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( private fun onContinue() { when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered()) if (checkAccessCodeMinChars().not()) return _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen()) } OnboardingVisaAccessCodeUM.Step.ReEnter -> { if (checkAccessCodesMatch().not()) return - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa()) startActivationProcess(accessCode = uiState.value.accessCodeFirst.text) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index ba23fac765..cf38445872 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -55,7 +55,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare()) } private fun getInitialState(): OnboardingVisaApproveUM { @@ -67,7 +67,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( private fun onApproveClick() { loading(true) - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove()) modelScope.launch { val dataToSign = diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 3e7fbaa01c..235c201961 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -61,7 +61,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen()) runShortPolling() } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 13e71e07f0..ace03f9a08 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -52,7 +52,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened()) modelScope.launch { while (true) { visaActivationRepository.getActivationRemoteState() @@ -81,12 +81,12 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink()) shareManager.shareText("https://tangem.com/") // TODO } private fun onOpenInBrowserClicked() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser()) urlOpener.openUrl("https://tangem.com/") // TODO } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 5ba1dd3aee..ae3bf7d96e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -47,7 +47,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened()) } private fun getInitialState(): OnboardingVisaPinCodeUM { @@ -67,12 +67,12 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( if (PinCodeValidation.validateAllDigits(pin)) { val isError = PinCodeValidation.validateLength(pin) && PinCodeValidation.validate(pin).not() - _uiState.update { - it.copy( + _uiState.update { currentState -> + currentState.copy( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), error = if (isError) { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation()) resourceReference(R.string.visa_onboarding_pin_validation_error_message) } else { null @@ -83,7 +83,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onSubmitClick() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered()) val pinCode = _uiState.value.pinCode if (PinCodeValidation.validate(pinCode).not()) return diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index 9e63e68fc3..8660ff65d0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt @@ -50,7 +50,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened()) } private fun getInitialState(): OnboardingVisaWelcomeUM { @@ -65,7 +65,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( } private fun onContinueClick() { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate()) if (params !is Config.WelcomeBack) { modelScope.launch { onDone.emit(DoneEvent.WelcomeDone) } return diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt index 25759dce3a..f2abbfba47 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt @@ -7,27 +7,27 @@ internal sealed class OnboardingVisaAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Onboarding / Visa", event, params) { - data object ActivationScreenOpened : OnboardingVisaAnalyticsEvent( + class ActivationScreenOpened : OnboardingVisaAnalyticsEvent( event = "Activation Screen Opened", ) - data object ButtonActivate : OnboardingVisaAnalyticsEvent( + class ButtonActivate : OnboardingVisaAnalyticsEvent( event = "Button - Activate", ) - data object SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent( + class SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent( event = "Setting Access Code Started", ) - data object AccessCodeEntered : OnboardingVisaAnalyticsEvent( + class AccessCodeEntered : OnboardingVisaAnalyticsEvent( event = "Access Code Entered", ) - data object AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent( + class AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent( event = "Access Code Re-enter Screen", ) - data object OnboardingVisa : OnboardingVisaAnalyticsEvent( + class OnboardingVisa : OnboardingVisaAnalyticsEvent( event = "Onboarding / Visa", ) @@ -38,48 +38,48 @@ internal sealed class OnboardingVisaAnalyticsEvent( params = mapOf("Type" to type), ) - data object WalletPrepare : OnboardingVisaAnalyticsEvent( + class WalletPrepare : OnboardingVisaAnalyticsEvent( event = "Wallet Prepare", ) - data object ButtonApprove : OnboardingVisaAnalyticsEvent( + class ButtonApprove : OnboardingVisaAnalyticsEvent( event = "Button - Approve", ) - data object GoToWebsiteOpened : OnboardingVisaAnalyticsEvent( + class GoToWebsiteOpened : OnboardingVisaAnalyticsEvent( event = "Go To Website Opened", ) - data object ButtonBrowser : OnboardingVisaAnalyticsEvent( + class ButtonBrowser : OnboardingVisaAnalyticsEvent( event = "Button - Browser", ) - data object ButtonShareLink : OnboardingVisaAnalyticsEvent( + class ButtonShareLink : OnboardingVisaAnalyticsEvent( event = "Button - Share Link", ) - data object ActivationInProgressScreen : OnboardingVisaAnalyticsEvent( + class ActivationInProgressScreen : OnboardingVisaAnalyticsEvent( event = "Activation In Progress Screen", ) - data object PinCodeScreenOpened : OnboardingVisaAnalyticsEvent( + class PinCodeScreenOpened : OnboardingVisaAnalyticsEvent( event = "PIN Code Screen Opened", ) - data object PinEntered : OnboardingVisaAnalyticsEvent( + class PinEntered : OnboardingVisaAnalyticsEvent( event = "PIN Entered", params = mapOf("Type" to "Visa"), ) - data object BiometricScreenOpened : OnboardingVisaAnalyticsEvent( + class BiometricScreenOpened : OnboardingVisaAnalyticsEvent( event = "Biometric Screen Opened", ) - data object SuccessScreenOpened : OnboardingVisaAnalyticsEvent( + class SuccessScreenOpened : OnboardingVisaAnalyticsEvent( event = "Success Screen Opened", ) - data object ErrorPinValidation : OnboardingVisaAnalyticsEvent( + class ErrorPinValidation : OnboardingVisaAnalyticsEvent( event = "Error - Pin Validation", ) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 6035f80287..4a29045354 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -86,7 +86,7 @@ internal class AllOffersStateFactory( fun getPaymentsState(): AllOffersStateUM { return when (val currentState = currentStateProvider.invoke()) { is AllOffersStateUM.Content -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) currentState.copy(currentMethod = null) } AllOffersStateUM.Loading, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index cf2c6af946..b29663f8cc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -48,8 +48,8 @@ internal class AllOffersModel @Inject constructor( init { subscribeOnAllOffers() - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) - analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) + analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked()) } fun dismiss() { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index 989bebc335..e2d8f30195 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -69,7 +69,7 @@ internal class ConfirmResidencyModel @Inject constructor( } else { ConfirmResidencyUM.ActionButtonConfig( onClick = { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() }, text = resourceReference(R.string.common_close), @@ -77,7 +77,7 @@ internal class ConfirmResidencyModel @Inject constructor( } private fun onChangeClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange) + analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange()) bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry(params.onDismiss)) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index b3569cec78..24010b69e9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -240,11 +240,11 @@ internal class OnrampAmountStateFactory( val errorTextRes = when (error) { is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) R.string.onramp_max_amount_restriction } is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) R.string.onramp_min_amount_restriction } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 18b31bb447..7c501be535 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -288,7 +288,7 @@ internal class OnrampMainComponentModel @Inject constructor( } override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) } @@ -333,7 +333,7 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt index 2af5055bd7..c46937ac48 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt @@ -112,11 +112,11 @@ internal class OnrampV2AmountStateFactory( val errorTextRes = when (error) { is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) R.string.onramp_max_amount_restriction } is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) R.string.onramp_min_amount_restriction } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt index 1fc8da3ee7..82de8c4f75 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt @@ -124,7 +124,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( } override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) } @@ -225,7 +225,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( } private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index 7c5493ae04..95cab56383 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -66,7 +66,7 @@ internal class SelectProviderModel @Inject constructor( userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened()) getPaymentMethods() getProviders(params.selectedPaymentMethod) } @@ -140,7 +140,7 @@ internal class SelectProviderModel @Inject constructor( } private fun openPaymentMethods() { - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) bottomSheetNavigation.activate( ProviderListBottomSheetConfig.PaymentMethods( selectedMethodId = state.value.selectedPaymentMethod.paymentMethod.id, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index 8c22aeb96a..a383d11e9a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -60,7 +60,7 @@ internal class OnrampSelectCountryModel @Inject constructor( val state: StateFlow get() = controller.state init { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened()) updateCountriesList() modelScope.launch { subscribeOnUpdateState() } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index cad8e72a0a..1c618de087 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -60,8 +60,8 @@ internal class OnrampOperationModel @Inject constructor( init { analyticsEventHandler.send( event = when (params) { - is Params.Buy -> MainScreenAnalyticsEvent.BuyScreenOpened - is Params.Sell -> MainScreenAnalyticsEvent.SellScreenOpened + is Params.Buy -> MainScreenAnalyticsEvent.BuyScreenOpened() + is Params.Sell -> MainScreenAnalyticsEvent.SellScreenOpened() }, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt index e7eba0a83f..7c6d7899b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt @@ -37,7 +37,7 @@ internal class OnrampSettingsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { - analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened()) subscribeOnUpdateState() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index ad9f3c8d31..5f318b6a4f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -74,7 +74,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( } override fun goToProviderClick(providerLink: String) { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) urlOpener.openUrl(providerLink) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index 5040c07c57..d9b66c559d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -50,7 +50,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ) init { - analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened) + analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened()) } @Composable diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index 20a2fe5ecd..f92ffd3549 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -4,12 +4,12 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { - data object ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") - data object ClickParticipate : ReferralEvents(event = "Button - Participate") - data object ClickCopy : ReferralEvents(event = "Button - Copy") - data object ClickShare : ReferralEvents(event = "Button - Share") - data object ClickTaC : ReferralEvents(event = "Link - TaC") - data object ParticipateSuccessful : ReferralEvents(event = "Participate Successful") + class ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") + class ClickParticipate : ReferralEvents(event = "Button - Participate") + class ClickCopy : ReferralEvents(event = "Button - Copy") + class ClickShare : ReferralEvents(event = "Button - Share") + class ClickTaC : ReferralEvents(event = "Link - TaC") + class ParticipateSuccessful : ReferralEvents(event = "Participate Successful") } private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index fd6d531d4f..ac7b852a3a 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -90,7 +90,7 @@ internal class ReferralModel @Inject constructor( } init { - analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened) + analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened()) if (accountsFeatureToggles.isFeatureEnabled) { combine( flow = referralData.filterNotNull().onEach(::selectAccount), @@ -178,7 +178,7 @@ internal class ReferralModel @Inject constructor( if (userWallet is UserWallet.Cold && isDemoCardUseCase(cardId = userWallet.cardId)) { showErrorSnackbar(DemoModeException()) } else { - analyticsEventHandler.send(ReferralEvents.ClickParticipate) + analyticsEventHandler.send(ReferralEvents.ClickParticipate()) val lastInfoState = uiState.referralInfoState uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { @@ -187,9 +187,9 @@ internal class ReferralModel @Inject constructor( false -> PortfolioId(params.userWalletId) } runCatching { referralInteractor.startReferral(portfolioId) } - .onSuccess { - analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) - referralData.value = it + .onSuccess { referral -> + analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful()) + referralData.value = referral } .onFailure { throwable -> if (throwable is ReferralError.UserCancelledException) { @@ -213,17 +213,17 @@ internal class ReferralModel @Inject constructor( } private fun onAgreementClicked() { - analyticsEventHandler.send(ReferralEvents.ClickTaC) + analyticsEventHandler.send(ReferralEvents.ClickTaC()) lastReferralData?.tosLink?.let(urlOpener::openUrl) } private fun onCopyClicked() { - analyticsEventHandler.send(ReferralEvents.ClickCopy) + analyticsEventHandler.send(ReferralEvents.ClickCopy()) } private fun onShareClicked(text: String) { - analyticsEventHandler.send(ReferralEvents.ClickShare) + analyticsEventHandler.send(ReferralEvents.ClickShare()) shareManager.shareText(text = text) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index e4b046c12c..48e2fd54fc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -466,7 +466,7 @@ internal class StakingModel @Inject constructor( } override fun onInitialInfoBannerClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking) + analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking()) innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -512,7 +512,7 @@ internal class StakingModel @Inject constructor( } override fun onMaxValueClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax()) stateController.update( AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -554,7 +554,7 @@ internal class StakingModel @Inject constructor( } override fun openRewardsValidators() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards()) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data @@ -878,7 +878,7 @@ internal class StakingModel @Inject constructor( } override fun onExploreClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore()) val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl @@ -892,7 +892,7 @@ internal class StakingModel @Inject constructor( val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare()) if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) shareManager.shareText(txUrl) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 673dc355c0..294890807d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -81,12 +81,12 @@ internal class StakingStateRouter( } fun showRewardsValidators() { - analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened) + analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened()) stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } } private fun showAmount() { - analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened) + analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened()) stateController.update { it.copy(currentStep = StakingStep.Amount) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 8d8ed4d072..f9f65022bc 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -23,7 +23,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - data object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") + class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", @@ -43,7 +43,7 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + class ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") data class ButtonPermissionApproveClicked( val sendToken: String, @@ -58,9 +58,9 @@ sealed class SwapEvents( ), ) - data object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") + class ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - data object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") data class SwapInProgressScreen( val provider: SwapProvider, @@ -81,7 +81,7 @@ sealed class SwapEvents( ), ) - data object ProviderClicked : SwapEvents("Provider Clicked") + class ProviderClicked : SwapEvents("Provider Clicked") data class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", @@ -98,7 +98,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - data object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") + class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index ffa4642c46..b9ed696598 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -375,7 +375,7 @@ internal class SwapModel @Inject constructor( ) { // exceptional case if (selectedCurrency == null) { - analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap) + analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap()) uiState = stateBuilder.createNoAvailableTokensToSwapState( uiStateHolder = uiState, fromToken = initialFromStatus, @@ -1271,7 +1271,7 @@ internal class SwapModel @Inject constructor( private fun onAmountSelected(selected: Boolean) { if (selected) { - analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked) + analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked()) } } @@ -1312,7 +1312,7 @@ internal class SwapModel @Inject constructor( }, onChangeCardsClicked = { onChangeCardsClicked() - analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked) + analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked()) }, onBackClicked = { val bottomSheet = uiState.bottomSheetConfig @@ -1331,10 +1331,10 @@ internal class SwapModel @Inject constructor( onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() - analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked()) uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -1362,7 +1362,7 @@ internal class SwapModel @Inject constructor( } }, onProviderClick = { providerId -> - analyticsEventHandler.send(SwapEvents.ProviderClicked) + analyticsEventHandler.send(SwapEvents.ProviderClicked()) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(providerId, states) uiState = stateBuilder.showSelectProviderBottomSheet( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index bfe48f97c9..774b5f2413 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -337,7 +337,7 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onClickSwap(data: TangemPayTopUpData) { - analytics.send(TangemPayAnalyticsEvents.SwapClicked) + analytics.send(TangemPayAnalyticsEvents.SwapClicked()) bottomSheetNavigation.dismiss() router.push( AppRoute.Swap( @@ -356,7 +356,7 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onClickReceive(data: TangemPayTopUpData) { - analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked) + analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked()) bottomSheetNavigation.dismiss() val config = TokenReceiveConfig( shouldShowWarning = false, 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 17e4ea67e5..92f6853e72 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 @@ -50,7 +50,7 @@ internal class TangemPayOnboardingModel @Inject constructor( field = MutableStateFlow(getInitialState()) init { - analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened) + analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened()) modelScope.launch { when (params) { is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { @@ -118,13 +118,13 @@ internal class TangemPayOnboardingModel @Inject constructor( } private fun onTermsClick() { - analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked) + analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked()) urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } @Suppress("NullableToStringCall") private fun onGetCardClick() { - analytics.send(TangemPayAnalyticsEvents.GetCardClicked) + analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) modelScope.launch { // TODO implement selector diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 809fa4ea3f..1ec6d138b7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -878,7 +878,7 @@ internal class TokenDetailsModel @Inject constructor( PromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, program = PromoAnalyticsEvent.Program.Empty, // Use it on new promo action - action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, + action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed(), ), ) } @@ -891,7 +891,7 @@ internal class TokenDetailsModel @Inject constructor( PromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, program = PromoAnalyticsEvent.Program.Empty, // Use it on new promo action - action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, + action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked(), ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index ec37185065..742e94dab2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -83,9 +83,9 @@ internal class TokenDetailsOnrampTransactionStateConverter( fallbackResId = R.drawable.ic_currency_24, ), iconState = getIconState(value.status), - onGoToProviderClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(it) + onGoToProviderClick = { url -> + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) + clickIntents.onGoToProviderClick(url) }, onClick = { clickIntents.onExpressTransactionClick(value.txId) }, onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, @@ -117,7 +117,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) clickIntents.onGoToProviderClick(externalTxUrl) } } else { @@ -269,7 +269,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( icon = R.drawable.ic_arrow_top_right_24, text = resourceReference(R.string.common_go_to_provider), onClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) clickIntents.onGoToProviderClick(externalTxUrl) }, ) 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 a44d3ead1c..6b4d8784d1 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 @@ -220,7 +220,7 @@ internal class WalletSettingsModel @Inject constructor( }, onReferralClick = { onReferralClick(userWallet) }, onManageTokensClick = { - analyticsEventHandler.send(Settings.ButtonManageTokens) + analyticsEventHandler.send(Settings.ButtonManageTokens()) router.push( AppRoute.ManageTokens( source = Source.SETTINGS, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index 97057fd80e..fb310ec8be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -95,7 +95,7 @@ internal class OrganizeTokensModel @Inject constructor( val onBack = MutableSharedFlow() init { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened()) getBalanceHidingSettingsUseCase() .onEach { @@ -117,7 +117,7 @@ internal class OrganizeTokensModel @Inject constructor( val list = cachedAccountStatusList ?: return if (list.sortType == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) modelScope.launch { toggleTokenListSortingUseCaseV2(list).fold( @@ -132,7 +132,7 @@ internal class OrganizeTokensModel @Inject constructor( val list = cachedTokenList ?: return if (list.sortedBy == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) modelScope.launch { toggleTokenListSortingUseCase(list).fold( @@ -150,7 +150,7 @@ internal class OrganizeTokensModel @Inject constructor( if (accountsFeatureToggles.isFeatureEnabled) { val list = cachedAccountStatusList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) modelScope.launch { toggleTokenListGroupingUseCaseV2(list).fold( @@ -164,7 +164,7 @@ internal class OrganizeTokensModel @Inject constructor( } else { val list = cachedTokenList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) modelScope.launch { toggleTokenListGroupingUseCase(list).fold( @@ -228,7 +228,7 @@ internal class OrganizeTokensModel @Inject constructor( } override fun onCancelClick() { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel()) modelScope.launch { onBack.emit(Unit) } } 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 60d86ab17b..586ccfc586 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 @@ -118,7 +118,7 @@ internal class WalletModel @Inject constructor( init { if (!hotWalletFeatureToggles.isHotWalletEnabled) { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy()) } suggestToOpenMarkets() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 726ed11fa8..db28dc4b12 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -51,7 +51,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onBalancesAndLimitsClick() { - analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked) + analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked()) modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() val balancesAndLimits = getVisaCurrencyUseCase(userWalletId) @@ -92,7 +92,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onExploreClick(exploreUrl: String) { - analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore) + analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore()) router.openUrl(exploreUrl) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt index 466f15890e..ba738c15b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt @@ -54,7 +54,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( ) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { - analyticsEventHandler.send(MainScreen.EditWalletTapped) + analyticsEventHandler.send(MainScreen.EditWalletTapped()) router.dialogNavigation.activate( configuration = WalletDialogConfig.RenameWallet( @@ -65,7 +65,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( } override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { - analyticsEventHandler.send(MainScreen.DeleteWalletTapped) + analyticsEventHandler.send(MainScreen.DeleteWalletTapped()) walletEventSender.send( event = WalletEvent.ShowAlert( 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 23a5a6ea50..7f79c77cb4 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 @@ -141,7 +141,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { - analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped) + analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped()) prepareAndStartOnboardingProcess() } @@ -169,7 +169,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) - analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) + analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped()) modelScope.launch { val userWallet = getSelectedUserWallet() ?: return@launch @@ -187,7 +187,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onOpenUnlockWalletsBottomSheetClick() { - analyticsEventHandler.send(MainScreen.WalletUnlockTapped) + analyticsEventHandler.send(MainScreen.WalletUnlockTapped()) if (hotWalletFeatureToggles.isHotWalletEnabled) { modelScope.launch { @@ -218,7 +218,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( @Deprecated("Will be removed with hot wallet release") override fun onUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics) + analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics()) modelScope.launch(dispatchers.main) { unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT) @@ -246,7 +246,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( @Deprecated("Will be removed with hot wallet release") override fun onScanToUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockWithCardScan) + analyticsEventHandler.send(MainScreen.UnlockWithCardScan()) openScanCardDialog() } @@ -304,17 +304,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onClosePromoClick(promoId: PromoId) { analyticsEventHandler.send( when (promoId) { - PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss + PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss() PromoId.Sepa -> PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.Sepa, - action = PromotionBannerClicked.BannerAction.Closed, + action = PromotionBannerClicked.BannerAction.Closed(), ) - PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss + PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss() PromoId.BlackFriday -> PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.BlackFriday, - action = PromotionBannerClicked.BannerAction.Closed, + action = PromotionBannerClicked.BannerAction.Closed(), ) }, @@ -328,7 +328,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( val userWallet = getSelectedUserWallet() ?: return when (promoId) { PromoId.Referral -> { - analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate) + analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate()) appRouter.push(ReferralProgram(userWalletId = userWallet.walletId)) } PromoId.Sepa -> { @@ -336,7 +336,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.Sepa, - action = PromotionBannerClicked.BannerAction.Clicked, + action = PromotionBannerClicked.BannerAction.Clicked(), ), ) cryptoCurrency ?: return @@ -350,7 +350,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) } PromoId.VisaPresale -> { - analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin) + analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin()) urlOpener.openUrl(VISA_PROMO_LINK) } PromoId.BlackFriday -> { @@ -358,7 +358,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.BlackFriday, - action = PromotionBannerClicked.BannerAction.Clicked, + action = PromotionBannerClicked.BannerAction.Clicked(), ), ) urlOpener.openUrl(BLACK_FRIDAY_PROMO_LINK) @@ -376,7 +376,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onNoteMigrationButtonClick(url: String) { - analyticsEventHandler.send(MainScreen.NotePromoButton) + analyticsEventHandler.send(MainScreen.NotePromoButton()) modelScope.launch(dispatchers.main) { router.openUrl(url) } @@ -385,7 +385,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseNotificationConfirm() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -408,7 +408,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseNotificationDecline() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -427,7 +427,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseSecondNotificationAccept() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -450,7 +450,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseSecondNotificationReject() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined()) modelScope.launch { seedPhraseNotificationUseCase.rejectSecond(userWalletId = userWallet.walletId) @@ -458,13 +458,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onFinishWalletActivationClick(isBackupExists: Boolean) { - analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation) + analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation()) val userWalletId = stateHolder.getSelectedWalletId() appRouter.push(WalletActivation(userWalletId, isBackupExists)) } override fun onAllowPermissions() { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush) + analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush()) walletEventSender.send( event = WalletEvent.RequestPushPermissions( onAllow = { @@ -490,7 +490,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onDenyPermissions() { modelScope.launch { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush) + analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush()) updateSubscribeOnPushPermissions(false) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index e7f0ce0a1f..5c151cb3ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -49,7 +49,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( ) : PromoDeeplinkHandler { init { - analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) + analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart()) val promoCode = queryParams[PROMO_CODE_KEY].orEmpty() if (promoCode.isEmpty()) { showAlert(InvalidPromoCode) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt index bd1bbc7dcc..ff1f1e2c1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt @@ -8,7 +8,7 @@ sealed class PromoActivationAnalytics( params: Map = mapOf(), ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { - data object PromoDeepLinkActivationStart : PromoActivationAnalytics( + class PromoDeepLinkActivationStart : PromoActivationAnalytics( event = "Bitcoin Promo Deep Link Activation", params = emptyMap(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt index 4d5ccf9b2c..a0c96c2974 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -8,11 +8,11 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Portfolio / Organize Tokens", event, params) { - object ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened") + class ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened") - object ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance") + class ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance") - object Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group") + class Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group") class Apply( grouping: AnalyticsParam.OnOffState, @@ -25,5 +25,5 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent( ), ) - object Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel") + class Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel") } \ No newline at end of file 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 00a748830b..bdd61799ba 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,7 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - data object ScreenOpenedLegacy : MainScreen( + class ScreenOpenedLegacy : MainScreen( event = "Screen opened", ) @@ -72,7 +72,7 @@ sealed class WalletScreenAnalyticsEvent { } } - data object ButtonFinalizeActivation : MainScreen( + class ButtonFinalizeActivation : MainScreen( event = "Button - Finalize Activation", ) @@ -93,9 +93,9 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Result" to result.value), ) - data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") - data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") - data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") + class NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") + class NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") + class WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") class NetworksUnreachable( tokens: List, @@ -104,54 +104,54 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Tokens" to tokens.joinToString()), ) - data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") + class MissingAddresses : MainScreen(event = "Notice - Missing Addresses") - data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") + class CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") - data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") + class HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") - data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") + class ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") - data object TestnetCard : MainScreen(event = "Notice - Testnet Card") + class TestnetCard : MainScreen(event = "Notice - Testnet Card") - data object DemoCard : MainScreen(event = "Notice - Demo Card") + class DemoCard : MainScreen(event = "Notice - Demo Card") - data object DevelopmentCard : MainScreen(event = "Notice - Development Card") + class DevelopmentCard : MainScreen(event = "Notice - Development Card") - data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") + class WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") - data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") + class BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") - data object BackupError : MainScreen(event = "Notice - Backup Error") + class BackupError : MainScreen(event = "Notice - Backup Error") - data object NotePromo : MainScreen(event = "Notice - Note Promo") + class NotePromo : MainScreen(event = "Notice - Note Promo") - data object NotePromoButton : MainScreen(event = "Note Promo Button") + class NotePromoButton : MainScreen(event = "Note Promo Button") - data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") + class UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") - data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") + class UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") - data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + class EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") - data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") + class DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") - data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") + class NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") - data object NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") + class NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") - data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") + class NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") - data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") + class NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") - data object NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") + class NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") - data object NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") + class NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") // region Referral Promo - data object ReferralPromo : MainScreen(event = "Referral Banner") - data object ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate") - data object ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss") + class ReferralPromo : MainScreen(event = "Referral Banner") + class ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate") + class ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss") //endregion } @@ -160,8 +160,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Promo", event = event, params = params) { - data object PushBanner : PushBannerPromo(event = "Push Banner") - data object ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push") - data object ButtonLaterPush : PushBannerPromo(event = "Button - Later Push") + class PushBanner : PushBannerPromo(event = "Push Banner") + class ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push") + class ButtonLaterPush : PushBannerPromo(event = "Button - Later Push") } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt index 90e94e979a..ad5cc6aa22 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt @@ -28,7 +28,7 @@ internal class SelectedWalletAnalyticsSender @Inject constructor( * that cannot be processed in [WalletWarningsAnalyticsSender]. * */ private fun getEvent(userWallet: UserWallet): AnalyticsEvent? = when { - userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock + userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock() else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt index ff945a2974..6c1b1a5176 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt @@ -27,8 +27,8 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor( val event = when { customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore cancelled state on analytics !customerInfo.info.isKycApproved -> return // ignore kyc not approved state on analytics - cardInfo != null && productInstance != null -> TangemPayAnalyticsEvents.MainScreenOpened - else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed + cardInfo != null && productInstance != null -> TangemPayAnalyticsEvents.MainScreenOpened() + else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed() } if (sentEvents.add(event)) { 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 de9c90b20e..66ad6e1bd0 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 @@ -39,16 +39,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { - is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard - is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard - is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet - is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions - is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard - is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard - is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses - is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem - is WalletNotification.Critical.BackupError -> MainScreen.BackupError - is WalletNotification.NoteMigration -> MainScreen.NotePromo + is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard() + is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard() + is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet() + is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions() + is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard() + is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard() + is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses() + is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem() + is WalletNotification.Critical.BackupError -> MainScreen.BackupError() + is WalletNotification.NoteMigration -> MainScreen.NotePromo() is WalletNotification.SwapPromo -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Empty, // Use it on new promo action @@ -61,8 +61,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( source = AnalyticsParam.ScreensSources.Main, program = Program.BlackFriday, ) - is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo - is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo + is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo() + is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo() is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, @@ -80,9 +80,9 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( } MainScreen.NoticeFinishActivation(activationState) } - is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport - is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond - is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner + is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport() + is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond() + is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner() is WalletNotification.Warning.TangemPayRefreshNeeded -> null WalletNotification.Warning.TangemPayUnreachable -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 7ea7a4d42a..9e11d7cd7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -154,7 +154,7 @@ internal class SetVisaInfoTransformer( clickIntents.onReceiveClick( userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - event = MainScreenAnalyticsEvent.ButtonReceive, + event = MainScreenAnalyticsEvent.ButtonReceive(), ) }, onLongClick = { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index eb2e240d23..4a2af49f86 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -44,12 +44,12 @@ internal class BalancesAndLimitsBottomSheetConverter( } private fun balanceInfoOnClick() { - analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo) + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo()) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) } private fun limitInfoOnClick(totalLimit: String, otherLimit: String) { - analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo) + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo()) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit))) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index 73813e73d3..3df7db2bb6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -82,9 +82,9 @@ internal class SingleWalletOnrampTransactionConverter( fallbackResId = R.drawable.ic_currency_24, ), iconState = getIconState(value.status), - onGoToProviderClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(it) + onGoToProviderClick = { url -> + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) + clickIntents.onGoToProviderClick(url) }, onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, onClick = { diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index e4fde45099..b6844fd970 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -115,7 +115,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -143,7 +145,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), @@ -172,7 +176,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), @@ -209,7 +215,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -248,7 +256,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -264,7 +274,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), @@ -280,7 +292,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), @@ -296,7 +310,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_no_address), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -312,7 +328,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.PromoCodeAlreadyUsed), @@ -425,7 +443,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -539,7 +559,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -594,7 +616,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -649,7 +673,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -700,7 +726,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -751,7 +779,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index fc0d38f971..57298f9b5d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -68,7 +68,7 @@ internal class WcConnectionsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { - analytics.send(WcAnalyticEvents.ScreenOpened) + analytics.send(WcAnalyticEvents.ScreenOpened()) listenQrUpdates() if (accountsFeatureToggles.isFeatureEnabled) { listenWcSessions() 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 37b00d61b2..0b1c498d8d 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 @@ -189,7 +189,7 @@ sealed class YieldSupplyAnalytics( ), ) - data object ApyChartViewed : YieldSupplyAnalytics( + class ApyChartViewed : YieldSupplyAnalytics( event = "APY Chart", ) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt index 642777e14b..a1384942e1 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt @@ -60,7 +60,7 @@ internal class YieldSupplyPromoModel @Inject constructor( } override fun onApyInfoClick() { - analytics.send(YieldSupplyAnalytics.ApyChartViewed) + analytics.send(YieldSupplyAnalytics.ApyChartViewed()) bottomSheetNavigation.activate(YieldSupplyPromoConfig.Apy) } From 788721e113503a51ebf0e285677cd553cd64e872 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 13:22:36 +0300 Subject: [PATCH 048/130] Updated on 2026-08-14 --- .../tangem/core/ui/message/EventMessage.kt | 3 +- .../DefaultUpgradeWalletComponent.kt | 1 + .../upgradewallet/UpgradeWalletModel.kt | 2 +- .../onboarding/v2/util/ResetCardsComponent.kt | 7 +- .../finalize/MultiWalletFinalizeComponent.kt | 1 + .../v2/util/impl/model/ResetCardsModel.kt | 136 +++++++++++++----- 6 files changed, 115 insertions(+), 35 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt index 173b9d2094..3da6b245e1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt @@ -246,7 +246,8 @@ data class EventMessageAction( * * @param onClick The action to perform when the button is clicked. By default, it dismisses the message. * */ - fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction( + fun cancelAction(isWarning: Boolean = false, onClick: () -> Unit = onDismissRequest) = EventMessageAction( + isWarning = isWarning, title = resourceReference(id = R.string.common_cancel), onClick = onClick, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt index 3775021494..dd8b450432 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -28,6 +28,7 @@ internal class DefaultUpgradeWalletComponent @AssistedInject constructor( private val resetCardsComponent = resetCardsComponentFactory.create( context = child("ResetCardsComponent"), params = ResetCardsComponent.Params( + source = ResetCardsComponent.Params.Source.Upgrade, callbacks = model.resetCardsComponentCallbacks, ), ) 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 dcaa94647a..9b177bc926 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 @@ -202,7 +202,7 @@ internal class UpgradeWalletModel @Inject constructor( } override fun onComplete() { - setLoading(false) + scanCard() } } } \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt index bd3a3203e2..6524a947c0 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt @@ -8,8 +8,13 @@ interface ResetCardsComponent { fun startResetCardsFlow(createdUserWallet: UserWallet.Cold) data class Params( + val source: Source, val callbacks: ModelCallbacks, - ) + ) { + enum class Source { + Upgrade, Onboarding, + } + } interface ModelCallbacks { fun onCancel() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt index 824770dc83..5bb2626fe5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt @@ -36,6 +36,7 @@ internal class MultiWalletFinalizeComponent( private val resetCardsComponent = resetCardsComponentFactory.create( context = child("ResetCardsComponent"), params = ResetCardsComponent.Params( + source = ResetCardsComponent.Params.Source.Onboarding, callbacks = model.resetCardsModelCallbacks, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt index 5a89688e94..ac0415702a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt @@ -1,6 +1,5 @@ package com.tangem.features.onboarding.v2.util.impl.model -import com.tangem.common.CompletionResult import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,12 +7,13 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.common.util.getBackupCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.util.ResetCardsComponent -import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -26,7 +26,7 @@ import kotlin.coroutines.resume internal class ResetCardsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val tangemSdkManager: TangemSdkManager, + private val resetCardUseCase: ResetCardUseCase, private val uiMessageSender: UiMessageSender, ) : Model() { @@ -41,14 +41,19 @@ internal class ResetCardsModel @Inject constructor( } private suspend fun startFullResetFlow(createdUserWallet: UserWallet.Cold) { + val userCodeParams = ResetCardUserCodeParams( + isAccessCodeSet = createdUserWallet.scanResponse.card.isAccessCodeSet, + isPasscodeSet = createdUserWallet.scanResponse.card.isPasscodeSet, + ) + suspendCancellableCoroutine { continuation -> uiMessageSender.send( DialogMessage.invoke( title = resourceReference(R.string.reset_cards_dialog_first_title), - isDismissable = false, + isDismissable = true, message = resourceReference(R.string.reset_cards_dialog_first_description), firstActionBuilder = { - cancelAction { + cancelAction(isWarning = true) { callbacks.onCancel() onDismissRequest() continuation.resume(Unit) @@ -57,13 +62,13 @@ internal class ResetCardsModel @Inject constructor( secondActionBuilder = { EventMessageAction( title = resourceReference(R.string.card_settings_action_sheet_reset), - isWarning = true, + isWarning = false, onClick = { modelScope.launch { - repeatUntilTrue { resetPrimaryCard(createdUserWallet) } + repeatUntilTrue { resetPrimaryCard(createdUserWallet, userCodeParams) } continuation.resume(Unit) onDismissRequest() - startResetBackupCardsFlow(createdUserWallet) + startResetBackupCardsFlow(createdUserWallet, userCodeParams) } }, ) @@ -74,7 +79,10 @@ internal class ResetCardsModel @Inject constructor( } } - private suspend fun startResetBackupCardsFlow(createdUserWallet: UserWallet.Cold) { + private suspend fun startResetBackupCardsFlow( + createdUserWallet: UserWallet.Cold, + userCodeParams: ResetCardUserCodeParams, + ) { val backupCardsCount = createdUserWallet.scanResponse.getBackupCardsCount() ?: 0 if (backupCardsCount == 0) { @@ -82,6 +90,8 @@ internal class ResetCardsModel @Inject constructor( return } + var canceled = false + repeat(backupCardsCount) { index -> suspendCancellableCoroutine { continuation -> uiMessageSender.send( @@ -90,23 +100,43 @@ internal class ResetCardsModel @Inject constructor( isDismissable = false, message = resourceReference(R.string.reset_cards_dialog_next_device_description), firstActionBuilder = { - cancelAction { - callbacks.onCancel() + cancelAction(isWarning = true) { onDismissRequest() - continuation.resume(Unit) + recommendResetAgain( + onCancel = { + canceled = true + callbacks.onCancel() + continuation.resume(Unit) + }, + onReset = onReset@{ + modelScope.launch { + repeatUntilTrue { + resetBackupCard( + cardNumber = index + 2, + params = userCodeParams, + userWalletId = createdUserWallet.walletId, + ) + } + this@onReset.onDismissRequest() + continuation.resume(Unit) + } + }, + ) } }, secondActionBuilder = { EventMessageAction( title = resourceReference(R.string.card_settings_action_sheet_reset), - isWarning = true, onClick = { modelScope.launch { - repeatUntilTrue { resetBackupCard(index + 1, createdUserWallet.walletId) } - onDismissRequest() - if (index == backupCardsCount - 1) { - completeResetFlow() + repeatUntilTrue { + resetBackupCard( + cardNumber = index + 2, + params = userCodeParams, + userWalletId = createdUserWallet.walletId, + ) } + onDismissRequest() continuation.resume(Unit) } }, @@ -116,7 +146,35 @@ internal class ResetCardsModel @Inject constructor( ), ) } + + if (canceled) return } + + completeResetFlow() + } + + private fun recommendResetAgain(onCancel: () -> Unit, onReset: EventMessageAction.BuilderScope.() -> Unit) { + uiMessageSender.send( + DialogMessage.invoke( + title = resourceReference(R.string.card_reset_alert_incomplete_title), + isDismissable = false, + message = resourceReference(R.string.card_reset_alert_incomplete_message), + firstActionBuilder = { + cancelAction(isWarning = true) { + onCancel() + onDismissRequest() + } + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.card_settings_action_sheet_reset), + onClick = { + onReset() + }, + ) + }, + ), + ) } private suspend fun completeResetFlow() { @@ -125,10 +183,20 @@ internal class ResetCardsModel @Inject constructor( DialogMessage.invoke( title = resourceReference(R.string.card_settings_completed_reset_alert_title), isDismissable = false, - message = resourceReference(R.string.reset_cards_dialog_complete_description), + message = when (params.source) { + ResetCardsComponent.Params.Source.Upgrade -> + resourceReference(R.string.card_reset_alert_finish_message) + ResetCardsComponent.Params.Source.Onboarding -> + resourceReference(R.string.card_settings_completed_reset_alert_message) + }, firstActionBuilder = { EventMessageAction( - title = resourceReference(R.string.common_done), + title = when (params.source) { + ResetCardsComponent.Params.Source.Upgrade -> + resourceReference(R.string.card_reset_alert_finish_ok_button) + ResetCardsComponent.Params.Source.Onboarding -> + resourceReference(R.string.common_done) + }, onClick = { modelScope.launch { onDismissRequest() @@ -144,26 +212,30 @@ internal class ResetCardsModel @Inject constructor( } } - private suspend fun resetPrimaryCard(createdUserWallet: UserWallet.Cold): Boolean { + private suspend fun resetPrimaryCard( + createdUserWallet: UserWallet.Cold, + params: ResetCardUserCodeParams, + ): Boolean { val scanResponse = createdUserWallet.scanResponse - val result = tangemSdkManager.resetToFactorySettings( + val result = resetCardUseCase.invoke( cardId = scanResponse.card.cardId, - allowsRequestAccessCodeFromRepository = true, + params = params, ) - return when (result) { - is CompletionResult.Failure -> false - is CompletionResult.Success -> true - } + return result.isRight() } - private suspend fun resetBackupCard(cardIndex: Int, userWalletId: UserWalletId): Boolean { - val result = tangemSdkManager.resetBackupCard(cardIndex, userWalletId) - return when (result) { - is CompletionResult.Failure -> false - is CompletionResult.Success -> true - } + private suspend fun resetBackupCard( + cardNumber: Int, + params: ResetCardUserCodeParams, + userWalletId: UserWalletId, + ): Boolean { + return resetCardUseCase.invoke( + cardNumber = cardNumber, + params, + userWalletId, + ).isRight() } private inline fun repeatUntilTrue(action: () -> Boolean) { From facd6bb2ca923d25631fc915318829cce9cd331d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 15:46:42 +0500 Subject: [PATCH 049/130] Updated on 2026-08-14 --- .../analytics/WalletSettingsAnalyticEvents.kt | 24 ++++++++-------- .../CreateHardwareWalletModel.kt | 6 +++- .../upgradewallet/UpgradeWalletModel.kt | 8 ++++-- .../entry/WalletActivationModel.kt | 26 ++++++++++------- .../walletbackup/model/WalletBackupModel.kt | 28 +++++++++++++++---- .../model/WalletHardwareBackupModel.kt | 14 ++++++++-- .../model/WalletSettingsModel.kt | 7 ++--- 7 files changed, 76 insertions(+), 37 deletions(-) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 4a46695ee3..93d7d9b6e1 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -16,11 +16,11 @@ sealed class WalletSettingsAnalyticEvents( params = mapOf(STATUS to enabled.value), ) - object WalletSettingsScreenOpened : WalletSettingsAnalyticEvents( + class WalletSettingsScreenOpened : WalletSettingsAnalyticEvents( event = "Wallet Settings Screen Opened", ) - object ButtonBackup : WalletSettingsAnalyticEvents( + class ButtonBackup : WalletSettingsAnalyticEvents( event = "Button - Backup", ) @@ -32,13 +32,13 @@ sealed class WalletSettingsAnalyticEvents( ) data class BackupScreenOpened( - val isManualBackupEnabled: Boolean, + val isBackedUp: Boolean, ) : WalletSettingsAnalyticEvents( event = "Backup Screen Opened", - params = mapOf("Manual Backup" to if (isManualBackupEnabled) "Enabled" else "Disabled"), + params = mapOf("Manual Backup" to if (isBackedUp) "Yes" else "No"), ) - object ButtonRecoveryPhrase : WalletSettingsAnalyticEvents( + class ButtonRecoveryPhrase : WalletSettingsAnalyticEvents( event = "Button - Recovery phrase", ) @@ -59,27 +59,27 @@ sealed class WalletSettingsAnalyticEvents( } } - object ButtonHardwareUpdate : WalletSettingsAnalyticEvents( + class ButtonHardwareUpdate : WalletSettingsAnalyticEvents( event = "Button - Hardware Update", ) - object HardwareUpgradeScreenOpened : WalletSettingsAnalyticEvents( + class HardwareUpgradeScreenOpened : WalletSettingsAnalyticEvents( event = "Hardware Upgrade Screen Opened", ) - object ButtonCreateNewWallet : WalletSettingsAnalyticEvents( + class ButtonCreateNewWallet : WalletSettingsAnalyticEvents( event = "Button - Create New Wallet", ) - object ButtonUpgradeCurrent : WalletSettingsAnalyticEvents( + class ButtonUpgradeCurrent : WalletSettingsAnalyticEvents( event = "Button - Upgrade Current", ) - object CreateWalletScreenOpened : WalletSettingsAnalyticEvents( + class CreateWalletScreenOpened : WalletSettingsAnalyticEvents( event = "Create Wallet Screen Opened", ) - object HardwareBackupScreenOpened : WalletSettingsAnalyticEvents( + class HardwareBackupScreenOpened : WalletSettingsAnalyticEvents( event = "Hardware Backup Screen Opened", ) @@ -141,7 +141,7 @@ sealed class WalletSettingsAnalyticEvents( params = mapOf(AnalyticsParam.Key.SOURCE to source), ) - object ButtonStartUpgrade : WalletSettingsAnalyticEvents( + class ButtonStartUpgrade : WalletSettingsAnalyticEvents( event = "Button - Start Upgrade", ) 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 fabdc48487..2d4b71b076 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 @@ -6,6 +6,7 @@ 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.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -51,6 +52,7 @@ internal class CreateHardwareWalletModel @Inject constructor( private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, private val userWalletsListRepository: UserWalletsListRepository, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -64,10 +66,12 @@ internal class CreateHardwareWalletModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened()) } override fun onDestroy() { + trackingContextProxy.removeContext() super.onDestroy() } 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 9b177bc926..7b675b8238 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 @@ -9,6 +9,7 @@ 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.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -62,6 +63,7 @@ internal class UpgradeWalletModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -79,10 +81,12 @@ internal class UpgradeWalletModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened()) } override fun onDestroy() { + trackingContextProxy.removeContext() clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) super.onDestroy() } @@ -96,7 +100,7 @@ internal class UpgradeWalletModel @Inject constructor( private fun onContinueClick() { analyticsEventHandler.send(IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.Upgrade)) - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade()) scanCard() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index d8f16f65e8..7def0b0676 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -70,16 +70,16 @@ internal class WalletActivationModel @Inject constructor( } val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) - private val source = AnalyticsParam.ScreensSources.Main - private val action = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup + private val analyticsSource = AnalyticsParam.ScreensSources.Main + private val analyticsAction = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup init { trackingContextProxy.addHotWalletContext() if (startRoute is WalletActivationRoute.ManualBackupStart) { analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -160,8 +160,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupPhrase) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseScreen( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -172,8 +172,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupCheck) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseCheck( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -184,8 +184,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupCompleted) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.BackupCompleteScreen( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -193,6 +193,9 @@ internal class WalletActivationModel @Inject constructor( inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { override fun onContinueClick(userWalletId: UserWalletId) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened(source = analyticsSource.value), + ) stackNavigation.push(WalletActivationRoute.SetAccessCode) } @@ -201,6 +204,9 @@ internal class WalletActivationModel @Inject constructor( inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.ReEnterAccessCodeScreen(source = analyticsSource.value), + ) stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) } 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 69ed17c12f..f1a729835a 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 @@ -4,6 +4,7 @@ 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.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -25,6 +26,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @Suppress("LongParameterList") @@ -37,11 +39,14 @@ internal class WalletBackupModel @Inject constructor( private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, private val router: Router, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() + private val isScreenOpenedEventSent: AtomicBoolean = AtomicBoolean(false) + val uiState: StateFlow field = MutableStateFlow( WalletBackupUM( @@ -68,20 +73,33 @@ internal class WalletBackupModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.BackupScreenOpened(isManualBackupEnabled = true)) + trackingContextProxy.addHotWalletContext() getUserWalletUseCase.invokeFlow(params.userWalletId) .onEach { either -> either.fold( ifLeft = { Timber.e("Error on getting user wallet: $it") }, - ifRight = { - updateBackupStatuses(it) + ifRight = { userWallet -> + if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.BackupScreenOpened( + isBackedUp = userWallet.backedUp, + ), + ) + isScreenOpenedEventSent.set(true) + } + updateBackupStatuses(userWallet) }, ) }.launchIn(modelScope) } + override fun onDestroy() { + trackingContextProxy.removeContext() + super.onDestroy() + } + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> if (userWallet is UserWallet.Hot) { @@ -119,7 +137,7 @@ internal class WalletBackupModel @Inject constructor( } private fun onRecoveryPhraseClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase()) if (uiState.value.backedUp) { getUserWalletUseCase.invoke(params.userWalletId) .fold( @@ -162,7 +180,7 @@ internal class WalletBackupModel @Inject constructor( } private fun onHardwareWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate()) router.push(AppRoute.WalletHardwareBackup(params.userWalletId)) } } \ No newline at end of file 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 2e9c2580cd..31d5ebdf25 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 @@ -5,6 +5,7 @@ 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.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -50,6 +51,7 @@ internal class WalletHardwareBackupModel @Inject constructor( private val urlOpener: UrlOpener, private val getUserWalletUseCase: GetUserWalletUseCase, private val messageSender: UiMessageSender, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -115,10 +117,16 @@ internal class WalletHardwareBackupModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened()) showPurchaseBlockWithDelay() } + override fun onDestroy() { + trackingContextProxy.removeContext() + super.onDestroy() + } + private fun showPurchaseBlockWithDelay() { modelScope.launch { delay(SHOW_PURCHASE_BLOCK_DELAY) @@ -127,7 +135,7 @@ internal class WalletHardwareBackupModel @Inject constructor( } private fun onCreateNewWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet()) router.push(AppRoute.CreateHardwareWallet) } @@ -135,7 +143,7 @@ internal class WalletHardwareBackupModel @Inject constructor( val userWallet = getUserWalletUseCase.invoke(params.userWalletId) .getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") } if (userWallet is UserWallet.Hot) { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent()) if (!userWallet.backedUp) { messageSender.send(makeBackupAtFirstAlertBS) } else { 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 6b4d8784d1..6d95c6fa82 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 @@ -115,7 +115,7 @@ internal class WalletSettingsModel @Inject constructor( init { getUserWalletUseCase.invoke(params.userWalletId).onRight { wallet -> trackingContextProxy.addContext(wallet) - analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletSettingsScreenOpened) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletSettingsScreenOpened()) } fun combineUI(wallet: UserWallet) = combine( @@ -384,7 +384,6 @@ internal class WalletSettingsModel @Inject constructor( } private fun onUpgradeWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate) if (!state.value.isWalletBackedUp) { showMakeBackupAtFirstAlertBS( isUpgradeFlow = true, @@ -404,7 +403,7 @@ internal class WalletSettingsModel @Inject constructor( } private fun onBackupClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup()) router.push(AppRoute.WalletBackup(params.userWalletId)) } @@ -441,7 +440,7 @@ internal class WalletSettingsModel @Inject constructor( shouldSetAccessCode = true, analyticsSource = AnalyticsParam.ScreensSources.WalletSettings.value, analyticsAction = if (isUpgradeFlow) { - RecoveryPhraseScreenAction.Backup.value + RecoveryPhraseScreenAction.Upgrade.value } else { RecoveryPhraseScreenAction.AccessCode.value }, From cf0bce7e615ffc67c50b7c29b3088847c81d8073 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 16:14:43 +0500 Subject: [PATCH 050/130] Updated on 2026-08-14 --- .../analytics/DefaultTrackingContextProxy.kt | 6 ---- .../tap/common/analytics/events/SignIn.kt | 2 -- .../CardContextInterceptor.kt | 8 +++-- .../HotWalletContextInterceptor.kt | 13 ++++++- .../DefaultUserWalletsListRepository.kt | 15 ++++---- .../component/impl/DefaultRoutingComponent.kt | 15 ++++---- .../ui/userwallet/UserWalletUnlockError.kt | 12 +++++-- .../core/analytics/models/AnalyticsParam.kt | 2 +- .../core/analytics/models/event/SignIn.kt | 6 ++++ .../analytics/utils/TrackingContextProxy.kt | 2 -- .../card/analytics/IntroductionProcess.kt | 2 ++ .../CreateWalletStartModel.kt | 14 +++++--- .../details/model/UserWalletListModel.kt | 6 ++++ .../HotAccessCodeRequestModel.kt | 4 +++ .../intents/WalletWarningsClickIntents.kt | 1 + .../welcome/impl/model/WelcomeModel.kt | 34 +++++++++++-------- 16 files changed, 91 insertions(+), 51 deletions(-) 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 923b26cbf5..6174bb4473 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 @@ -71,12 +71,6 @@ 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/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index d34334ec34..21f5f23180 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -10,8 +10,6 @@ sealed class SignIn( params: Map = emptyMap(), ) : AnalyticsEvent("Sign In", event, params) { - class ScreenOpened : SignIn(event = "Sign In Screen Opened") - class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In") class ButtonCardSignIn : SignIn(event = "Button - Card Sign In") } \ No newline at end of file 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 c7bc17f8e9..cdd42cdde9 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 @@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.SignIn import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver @@ -30,8 +31,11 @@ class CardContextInterceptor( override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { return when (event) { - is IntroductionProcess.ButtonScanCardLegacy -> false - is IntroductionProcess.ButtonScanCard -> false + is IntroductionProcess.ButtonScanCard, + is IntroductionProcess.ButtonScanCardLegacy, + is SignIn.ScreenOpened, + is SignIn.ButtonAddWallet, + -> 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 ab9c9d3b54..f67eaab3f8 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 @@ -3,6 +3,8 @@ package com.tangem.tap.common.analytics.paramsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.SignIn +import com.tangem.domain.card.analytics.IntroductionProcess class HotWalletContextInterceptor( val parent: ParamsInterceptor? = null, @@ -10,7 +12,16 @@ class HotWalletContextInterceptor( override fun id(): String = HotWalletContextInterceptor.id() - override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { + return when (event) { + is SignIn.ScreenOpened, + is SignIn.ButtonAddWallet, + is SignIn.ButtonUnlockAllWithBiometric, + is IntroductionProcess.ButtonScanCard, + -> false + else -> true + } + } override fun intercept(params: MutableMap) { params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value 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 29f19ffb75..3064dd8840 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 @@ -525,13 +525,12 @@ internal class DefaultUserWalletsListRepository( } private fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) { - trackingContextProxy.proceedWithContext(userWallet) { - analyticsEventHandler.send( - event = Basic.SignedIn( - signInType = type, - walletsCount = userWallets.value?.size ?: 0, - ), - ) - } + trackingContextProxy.addContext(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/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 483891f435..9e4f210063 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 @@ -227,14 +227,13 @@ internal class DefaultRoutingComponent @AssistedInject constructor( 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, - ), - ) - } + trackingContextProxy.addContext(selectedWallet) + analyticsEventHandler.send( + event = Basic.SignedIn( + signInType = Basic.SignedIn.SignInType.NoSecurity, + walletsCount = userWallets.size, + ), + ) } } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index 851972f960..e7e4b2a884 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -1,6 +1,8 @@ package com.tangem.common.ui.userwallet import com.tangem.common.ui.R +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.SignIn import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -13,6 +15,7 @@ import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.R inline fun UnlockWalletError.handle( onAlreadyUnlocked: () -> Unit = {}, onUserCancelled: () -> Unit = {}, + analyticsEventHandler: AnalyticsEventHandler, noinline showMessage: (EventMessage) -> Unit, ) { when (this) { @@ -30,15 +33,20 @@ inline fun UnlockWalletError.handle( // This should never happen in this flow, as we always check for the wallet existence before unlocking showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error))) } - is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, showMessage) + is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, analyticsEventHandler, showMessage) } } -fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) { +fun handleUnableToUnlock( + error: UnlockWalletError.UnableToUnlock, + analyticsEventHandler: AnalyticsEventHandler, + showDialog: (DialogMessage) -> Unit, +) { val dialogMessage = when (error) { is UnlockWalletError.UnableToUnlock.WithReason -> { when (error.reason) { Reason.AllKeysInvalidated -> { + analyticsEventHandler.send(SignIn.ErrorBiometricUpdated()) DialogMessage( title = resourceReference(R.string.biometric_updated_warning_title), message = resourceReference(R.string.biometric_updated_warning_description), 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 7f051ebf87..8ea55f5139 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,7 +83,7 @@ sealed class AnalyticsParam { 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 CreateWalletIntro : ScreensSources("Create Wallet Intro") data object AddNewWallet : ScreensSources("Add New Wallet") data object CreateWallet : ScreensSources("Create Wallet") } 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 index 5d0e483b1c..9b88374046 100644 --- 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 @@ -17,13 +17,19 @@ sealed class SignIn( ), ) + class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In") + class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric") + class ErrorBiometricUpdated : SignIn(event = "Error - Biometric Updated") + class ButtonWallet( signInType: SignInType, + walletsCount: Int, ) : SignIn( event = "Button - Wallet", params = buildMap { + put("Wallets Count", walletsCount.toString()) put("Sign in type", signInType.value) }, ) { 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 30671f81d0..23be99e1fd 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,6 +23,4 @@ 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 b6ec1affa6..b7151afd3d 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 @@ -13,6 +13,8 @@ sealed class IntroductionProcess( class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") + class CreateWalletIntroScreenOpened : IntroductionProcess("Create Wallet Intro Screen Opened") + class ButtonScanCard( val source: AnalyticsParam.ScreensSources, ) : IntroductionProcess( 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 c72cfae7a2..554c0becb1 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 @@ -127,9 +127,15 @@ internal class CreateWalletStartModel @Inject constructor( }, ) + init { + analyticsEventHandler.send( + event = IntroductionProcess.CreateWalletIntroScreenOpened(), + ) + } + private fun onScanClick() { analyticsEventHandler.send( - event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateNewWallet), + event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWalletIntro), ) scanCard() } @@ -137,7 +143,7 @@ internal class CreateWalletStartModel @Inject constructor( private fun onStartWithMobileWalletClick() { analyticsEventHandler.send( event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( - source = AnalyticsParam.ScreensSources.CreateNewWallet.value, + source = AnalyticsParam.ScreensSources.CreateWalletIntro.value, ), ) if (!isHotWalletCreationSupported()) { @@ -147,11 +153,11 @@ internal class CreateWalletStartModel @Inject constructor( return } - router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateNewWallet.value)) + router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateWalletIntro.value)) } private fun onBuyClick() { - analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateNewWallet)) + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 923627f080..f0b9432ebf 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -3,6 +3,9 @@ package com.tangem.features.details.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.event.SignIn import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -39,6 +42,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletSaver: UserWalletSaver, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val unlockWalletUseCase: UnlockWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -87,6 +91,7 @@ internal class UserWalletListModel @Inject constructor( private fun onAddNewWalletClick() { if (hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) router.push(AppRoute.CreateWalletSelection) } else { withProgress(isWalletSavingInProgress) { @@ -105,6 +110,7 @@ internal class UserWalletListModel @Inject constructor( error.handle( onUserCancelled = {}, onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, + analyticsEventHandler = analyticsEventHandler, showMessage = messageSender::send, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index ad3e784dd7..ba2bbf512a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.hotwallet.accesscoderequest +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.SignIn import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.components.fields.PinTextColor @@ -33,6 +35,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val userWalletsListRepository: UserWalletsListRepository, private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val result = MutableStateFlow(null) @@ -108,6 +111,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( onAccessCodeChange = ::onAccessCodeChange, accessCode = "", useBiometricClick = { + analyticsEventHandler.send(SignIn.ButtonBiometricSignIn()) dismissState() result.value = HotWalletPasswordRequester.Result.UseBiometry }, 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 7f79c77cb4..05b094761f 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 @@ -199,6 +199,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( error.handle( onAlreadyUnlocked = {}, onUserCancelled = {}, + analyticsEventHandler = analyticsEventHandler, showMessage = uiMessageSender::send, ) } 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 c0a06bc9e6..249109f0e4 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 @@ -58,14 +58,18 @@ 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)) + trackingContextProxy.addContext(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( + event = SignIn.ButtonWallet( + signInType = signInType, + walletsCount = userWallets.size, + ), + ) onUserWalletClick(userWallet) } }, @@ -210,6 +214,7 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) }, onUserCancelled = { onUserCancelled() }, + analyticsEventHandler = analyticsEventHandler, showMessage = uiMessageSender::send, ) } @@ -226,13 +231,12 @@ 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, - ), - ) - } + trackingContextProxy.addContext(userWallet) + analyticsEventHandler.send( + event = Basic.SignedIn( + signInType = type, + walletsCount = walletsCount, + ), + ) } } \ No newline at end of file From 97bb0ab768cce5cb8c510af37cc6b88a3e8c17a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 16:14:57 +0500 Subject: [PATCH 051/130] Updated on 2026-08-14 --- .../presentation/wallet/state/model/WalletNotification.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 9793c879cb..454f96a6be 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 @@ -294,7 +294,11 @@ sealed class WalletNotification(val config: NotificationConfig) { ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), - subtitle = resourceReference(R.string.hw_activation_need_description), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, iconResId = R.drawable.img_knight_shield_32, iconTint = when (type) { WalletActivationBannerType.Attention -> IconTint.Attention From b0f22bcf477809202939c26fcc6a1c587535a9e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 14:42:26 +0300 Subject: [PATCH 052/130] Updated on 2026-08-14 --- .../di/UserWalletsListManagerModule.kt | 3 +++ .../repository/DefaultUserWalletsListRepository.kt | 13 ++++++++----- .../entry/AddExistingWalletModel.kt | 3 ++- .../walletactivation/entry/WalletActivationModel.kt | 3 ++- 4 files changed, 15 insertions(+), 7 deletions(-) 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 abe6b49ded..74ad98e889 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 @@ -10,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -127,6 +128,7 @@ internal object UserWalletsListManagerModule { tangemHotSdk: TangemHotSdk, trackingContextProxy: TrackingContextProxy, analyticsEventHandler: AnalyticsEventHandler, + hotWalletRepository: HotWalletRepository, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -176,6 +178,7 @@ internal object UserWalletsListManagerModule { tangemHotSdk = tangemHotSdk, trackingContextProxy = trackingContextProxy, analyticsEventHandler = analyticsEventHandler, + hotWalletRepository = hotWalletRepository, ) } 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 3064dd8840..80dce033a7 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 @@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.common.wallets.error.* +import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -55,6 +56,7 @@ internal class DefaultUserWalletsListRepository( private val tangemHotSdk: TangemHotSdk, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, + private val hotWalletRepository: HotWalletRepository, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -209,7 +211,7 @@ internal class DefaultUserWalletsListRepository( userWalletEncryptionKeysRepository.delete(userWalletIds) - removeHotWalletsFromSDK(userWalletIds) + removeHotWalletsFromSDKAndRepos(userWalletIds) userWallets.update { currentWallets -> val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() } @@ -388,7 +390,7 @@ internal class DefaultUserWalletsListRepository( if (newUserWallet.walletId == oldUserWallet.walletId && oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold ) { - removeHotWalletsFromSDK(walletIds = listOf(oldUserWallet.walletId)) + removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId)) // When upgrading from Hot to Cold, if biometric lock is available, set it if (hasBiometry()) { setLock(newUserWallet.walletId, LockMethod.Biometric, changeUnsecured = true) @@ -399,13 +401,14 @@ internal class DefaultUserWalletsListRepository( } } - private suspend fun removeHotWalletsFromSDK(walletIds: List) { + private suspend fun removeHotWalletsFromSDKAndRepos(walletIds: List) { val hotWalletsToDelete = userWalletsSync() .filterIsInstance() .filter { walletIds.contains(it.walletId) } - hotWalletsToDelete.forEach { - tangemHotSdk.delete(it.hotWalletId) + hotWalletsToDelete.forEach { wallet -> + hotWalletRepository.setAccessCodeSkipped(wallet.walletId, false) // In case the wallet is added again + tangemHotSdk.delete(wallet.hotWalletId) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index e40072474a..51cfc12211 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -27,6 +27,7 @@ import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -110,7 +111,7 @@ internal class AddExistingWalletModel @Inject constructor( title = resourceReference(R.string.access_code_alert_skip_ok), onClick = { if (userWalletId != null) { - modelScope.launch { + modelScope.launch(NonCancellable) { setAccessCodeSkippedUseCase(userWalletId, true) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 7def0b0676..3f91e6d246 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -33,6 +33,7 @@ import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -134,7 +135,7 @@ internal class WalletActivationModel @Inject constructor( secondAction = EventMessageAction( title = resourceReference(R.string.access_code_alert_skip_ok), onClick = { - modelScope.launch { + modelScope.launch(NonCancellable) { setAccessCodeSkippedUseCase(userWalletId, true) } navigateToPushNotificationsOrNext() From 364bc12ffe6a1e7122ff37bf2fe3b004b9400097 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 13:15:57 +0000 Subject: [PATCH 053/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 07be659f64..8c10bcaf9d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31-1314" +tangemBlockchainSdk = "develop-1317" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +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 d443530171d92e80a744bd91a333b53dcdae96db Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 09:42:12 +0300 Subject: [PATCH 054/130] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 3 + .../tangem/datasource/di/PromoStoreModule.kt | 9 ++ .../local/promo/DefaultPromoBannerStore.kt | 19 ++++ .../local/promo/PromoBannerStore.kt | 10 ++ .../data/promo/DefaultPromoRepository.kt | 15 +++ .../tangem/data/promo/di/PromoDataModule.kt | 3 + domain/onramp/build.gradle.kts | 1 + .../domain/onramp/GetOnrampOffersUseCase.kt | 35 +++++- .../onramp/GetOnrampOffersUseCaseTest.kt | 105 ++++++++++++++++++ .../tangem/domain/promo/PromoRepository.kt | 2 + 10 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0c20fd0993..34ffd4aaba 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides @@ -264,12 +265,14 @@ internal object OnrampDomainModule { onrampErrorResolver: OnrampErrorResolver, onrampTransactionRepository: OnrampTransactionRepository, settingsRepository: SettingsRepository, + promoRepository: PromoRepository, ): GetOnrampOffersUseCase { return GetOnrampOffersUseCase( onrampRepository = onrampRepository, errorResolver = onrampErrorResolver, onrampTransactionRepository = onrampTransactionRepository, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt index b8af7137ae..af846ebcbb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt @@ -1,7 +1,10 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.promo.DefaultPromoBannerStore import com.tangem.datasource.local.promo.DefaultPromoStoriesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import dagger.Module import dagger.Provides @@ -18,4 +21,10 @@ object PromoStoreModule { fun providePromoStoriesStore(): PromoStoriesStore { return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun providePromoBannerStore(): PromoBannerStore { + return DefaultPromoBannerStore(dataStore = RuntimeSharedStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt new file mode 100644 index 0000000000..6065fa079a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore + +internal class DefaultPromoBannerStore( + private val dataStore: RuntimeSharedStore>, +) : PromoBannerStore { + + override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? { + return dataStore.getSyncOrNull()?.get(promoId) + } + + override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) { + dataStore.update(emptyMap()) { current -> + current + (promoId to promoBanner) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt new file mode 100644 index 0000000000..f951238bd5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse + +interface PromoBannerStore { + + suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? + + suspend fun store(promoId: String, promoBanner: PromoBannerResponse) +} \ No newline at end of file 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 beb55966c3..1e465aa020 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 @@ -10,6 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStor import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository @@ -28,6 +29,7 @@ internal class DefaultPromoRepository( private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, private val promoStoriesStore: PromoStoriesStore, + private val promoBannerStore: PromoBannerStore, private val dispatchers: CoroutineDispatcherProvider, private val referralRepository: ReferralRepository, ) : PromoRepository { @@ -96,6 +98,18 @@ internal class DefaultPromoRepository( ) } + override suspend fun isMoonpayPromoActive(): Boolean { + val banner = runCatching(dispatchers.io) { + val response = promoBannerStore.getSyncOrNull(MOONPAY_NAME) ?: run { + val apiResponse = tangemApi.getPromoBanner(MOONPAY_NAME).getOrThrow() + promoBannerStore.store(MOONPAY_NAME, apiResponse) + apiResponse + } + promoBannerConverter.convert(response) + }.getOrNull() + return banner?.isActive == true + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } @@ -166,6 +180,7 @@ internal class DefaultPromoRepository( const val SEPA_NAME = "sepa" const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" + const val MOONPAY_NAME = "moonpay" const val STORIES_LOAD_DELAY = 1000L } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 69d489a2dc..2f0c689e53 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository import com.tangem.feature.referral.domain.ReferralRepository @@ -23,6 +24,7 @@ internal object PromoDataModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, promoStoriesStore: PromoStoriesStore, + promoBannerStore: PromoBannerStore, dispatchers: CoroutineDispatcherProvider, referralRepository: ReferralRepository, ): PromoRepository { @@ -32,6 +34,7 @@ internal object PromoDataModule { promoStoriesStore = promoStoriesStore, dispatchers = dispatchers, referralRepository = referralRepository, + promoBannerStore = promoBannerStore, ) } } \ No newline at end of file diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index 7823e8dcea..cd1f511092 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.domain.core) api(projects.domain.settings) implementation(deps.kotlin.serialization) + implementation(projects.domain.promo) /** Tests */ testImplementation(deps.test.coroutine) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 0b2aba9380..8bc240224a 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -11,9 +11,11 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class GetOnrampOffersUseCase( @@ -21,14 +23,16 @@ class GetOnrampOffersUseCase( private val onrampTransactionRepository: OnrampTransactionRepository, private val errorResolver: OnrampErrorResolver, private val settingsRepository: SettingsRepository, + private val promoRepository: PromoRepository, ) { operator fun invoke(): EitherFlow> { return combine( onrampRepository.getQuotes(), onrampTransactionRepository.getAllTransactions(), - ) { quotes, transactions -> - processOffers(quotes, transactions) + flow { emit(promoRepository.isMoonpayPromoActive()) }, + ) { quotes, transactions, isMoonpayPromoActive -> + processOffers(quotes, transactions, isMoonpayPromoActive) } .map { offers -> offers.right() } .catch { throwable -> errorResolver.resolve(throwable).left() } @@ -37,6 +41,7 @@ class GetOnrampOffersUseCase( private suspend fun processOffers( quotes: List, transactions: List, + isMoonpayPromoActive: Boolean, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() @@ -57,7 +62,7 @@ class GetOnrampOffersUseCase( val recentOffer = findRecentOffer(offers, transactions) val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) - val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable, isMoonpayPromoActive) return buildOffersBlocks( recentOffer = recentOffer, @@ -85,8 +90,24 @@ class GetOnrampOffersUseCase( return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { - val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } + private fun findFastestOffer( + offers: List, + isGooglePayAvailable: Boolean, + isMoonpayPromoActive: Boolean, + ): OnrampOffer? { + val moonpayPromoOffers = if (isMoonpayPromoActive) { + offers.filter { + it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + } + } else { + emptyList() + } + + val instantOffers = moonpayPromoOffers.ifEmpty { + offers.filter { it.quote.paymentMethod.type.isInstant() } + } + return if (instantOffers.isNotEmpty()) { instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable)) } else { @@ -287,4 +308,8 @@ class GetOnrampOffersUseCase( -> true } } + + private companion object { + const val MOONPAY_PROMO_PROVIDER_ID = "moonpay" + } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 660735a8e7..5f249f97a2 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf @@ -24,6 +25,7 @@ class GetOnrampOffersUseCaseTest { private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val promoRepository: PromoRepository = mockk(relaxUnitFun = true) private lateinit var useCase: GetOnrampOffersUseCase @@ -35,6 +37,7 @@ class GetOnrampOffersUseCaseTest { onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } @@ -225,6 +228,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { promoRepository.isMoonpayPromoActive() } returns false coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf( @@ -245,6 +249,107 @@ class GetOnrampOffersUseCaseTest { } } + @Test + fun `invoke should fallback to standard instant offers when promo is active but no Moonpay offers exist`() = + runTest { + val instantMethod = createMockPaymentMethod("gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val provider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(instantMethod, provider, BigDecimal("95.0")), + createMockQuote(slowMethod, provider, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(2) + + val fastestOffer = + recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isNotEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should show Moonpay fastest offer when promo is active`() = runTest { + val moonpayGooglePayMethod = createMockPaymentMethod("moonpay-gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val otherGooglePayMethod = createMockPaymentMethod( + "other-gpay", + "Other Google Pay", + PaymentMethodType.GOOGLE_PAY, + ) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val moonpayProvider = createMockProvider("moonpay", "Moonpay") + val otherProvider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(moonpayGooglePayMethod, moonpayProvider, BigDecimal("100.0")), + createMockQuote(otherGooglePayMethod, otherProvider, BigDecimal("95.0")), + createMockQuote(slowMethod, otherProvider, BigDecimal("105.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + + val fastestOffer = recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + private fun createMockPaymentMethod( id: String, name: String, 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 d8ea76a512..6e7afbdb02 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 @@ -19,6 +19,8 @@ interface PromoRepository { fun isMarketsStakingNotificationHideClicked(): Flow suspend fun setMarketsStakingNotificationHideClicked() + + suspend fun isMoonpayPromoActive(): Boolean // endregion // region Stories From 0f346ca611f02f89f98cdfa169be9dd910976e07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 14:42:57 +0300 Subject: [PATCH 055/130] Updated on 2026-08-14 --- .../v2/util/impl/model/ResetCardsModel.kt | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt index ac0415702a..20d87c7278 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt @@ -65,9 +65,9 @@ internal class ResetCardsModel @Inject constructor( isWarning = false, onClick = { modelScope.launch { - repeatUntilTrue { resetPrimaryCard(createdUserWallet, userCodeParams) } - continuation.resume(Unit) onDismissRequest() + resetOrCancel { resetPrimaryCard(createdUserWallet, userCodeParams) } + continuation.resume(Unit) startResetBackupCardsFlow(createdUserWallet, userCodeParams) } }, @@ -109,15 +109,16 @@ internal class ResetCardsModel @Inject constructor( continuation.resume(Unit) }, onReset = onReset@{ + this@onReset.onDismissRequest() + modelScope.launch { - repeatUntilTrue { + resetOrCancel { resetBackupCard( cardNumber = index + 2, params = userCodeParams, userWalletId = createdUserWallet.walletId, ) } - this@onReset.onDismissRequest() continuation.resume(Unit) } }, @@ -128,15 +129,17 @@ internal class ResetCardsModel @Inject constructor( EventMessageAction( title = resourceReference(R.string.card_settings_action_sheet_reset), onClick = { + onDismissRequest() + modelScope.launch { - repeatUntilTrue { + resetOrCancel { resetBackupCard( cardNumber = index + 2, params = userCodeParams, userWalletId = createdUserWallet.walletId, ) } - onDismissRequest() + continuation.resume(Unit) } }, @@ -212,14 +215,32 @@ internal class ResetCardsModel @Inject constructor( } } + private suspend fun resetOrCancel(action: suspend () -> Boolean) { + while (true) { + if (action()) { + return + } + + suspendCancellableCoroutine { continuation -> + recommendResetAgain( + onCancel = { + callbacks.onCancel() + continuation.cancel() + }, + onReset = { + continuation.resume(Unit) + }, + ) + } + } + } + private suspend fun resetPrimaryCard( createdUserWallet: UserWallet.Cold, params: ResetCardUserCodeParams, ): Boolean { - val scanResponse = createdUserWallet.scanResponse - val result = resetCardUseCase.invoke( - cardId = scanResponse.card.cardId, + cardId = createdUserWallet.scanResponse.card.cardId, params = params, ) @@ -233,14 +254,8 @@ internal class ResetCardsModel @Inject constructor( ): Boolean { return resetCardUseCase.invoke( cardNumber = cardNumber, - params, - userWalletId, - ).isRight() - } - - private inline fun repeatUntilTrue(action: () -> Boolean) { - while (true) { - if (action()) break - } + params = params, + userWalletId = userWalletId, + ).getOrNull() == true } } \ No newline at end of file From 822d5816f8ab9363b87eadf10209895d62bdf3fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 14:48:37 +0300 Subject: [PATCH 056/130] Updated on 2026-08-14 --- .../com/tangem/screens/SendPageObject.kt | 10 +- .../send/amountScreen/SendAmountScreenTest.kt | 145 ++++++++++++++++++ .../ui/amountScreen/ui/AmountFieldV2.kt | 1 + .../tangem/core/ui/test/SendScreenTestTags.kt | 1 + 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt index 1e1f1da9d2..6cc240110b 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt @@ -10,6 +10,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString import com.tangem.features.send.v2.impl.R as SendR +import androidx.compose.ui.test.hasText as withText class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -38,6 +39,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : useUnmergedTree = true } + val amountErrorText: KNode = child { + hasTestTag(SendScreenTestTags.AMOUNT_ERROR_TEXT) + useUnmergedTree = true + } + val equivalentInputAmount: KNode = child { hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT) useUnmergedTree = true @@ -69,8 +75,8 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : } val nextButton: KNode = child { - hasTestTag(BaseButtonTestTags.TEXT) - hasText(getResourceString(SendR.string.common_next)) + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyDescendant(withText(getResourceString(SendR.string.common_next))) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt new file mode 100644 index 0000000000..dd6f386748 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt @@ -0,0 +1,145 @@ +package com.tangem.tests.send.amountScreen + +import android.view.KeyEvent +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.setClipboardText +import com.tangem.scenarios.* +import com.tangem.screens.* +import com.tangem.wallet.R +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SendAmountScreenTest : BaseTestCase() { + + @AllureId("4761") + @DisplayName("Send (amount screen): validate different amounts") + @Test + fun validateDifferentAmountsTest() { + val tokenName = "Ethereum" + val manualSendAmount = "1" + val clipboardSendAmount = "0.5" + val invalidAmount = "2" + val errorText = getResourceString(R.string.send_validation_amount_exceeds_balance) + val context = device.context + + setupHooks().run { + step("Open 'Send' screen") { + openSendScreen(tokenName) + } + step("Type '$manualSendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(manualSendAmount) + } + } + step("Assert send amount = '$manualSendAmount'") { + onSendScreen { amountInputTextField.assertTextContains(manualSendAmount, substring = true) } + } + step("Assert 'Next' button is enabled") { + onSendScreen { nextButton.assertIsEnabled() } + } + step("Press system 'Delete' button") { + waitForIdle() + device.uiDevice.pressDelete() + } + step("Set clipboard text") { + setClipboardText(context, clipboardSendAmount) + } + step("Click on input text field") { + onSendScreen { amountInputTextField.performClick() } + } + step("Paste from clipboard") { + device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON) + } + step("Assert send amount = '$clipboardSendAmount'") { + onSendScreen { amountInputTextField.assertTextContains(clipboardSendAmount, substring = true) } + } + step("Assert 'Next' button is enabled") { + onSendScreen { nextButton.assertIsEnabled() } + } + step("Press system 'Delete' button") { + waitForIdle() + device.uiDevice.pressDelete() + } + step("Type '$invalidAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(invalidAmount) + } + } + step("Assert send amount = '$invalidAmount'") { + onSendScreen { amountInputTextField.assertTextContains(invalidAmount, substring = true) } + } + step("Assert amount error contains text '$errorText'") { + onSendScreen { amountErrorText.assertTextContains(errorText) } + } + step("Assert 'Next' button is disabled") { + onSendScreen { nextButton.assertIsNotEnabled() } + } + step("Click on 'Max' button") { + onSendScreen { maxButton.performClick() } + } + step("Assert send amount = '$manualSendAmount'") { + onSendScreen { amountInputTextField.assertTextContains(manualSendAmount, substring = true) } + } + step("Assert 'Next' button is enabled") { + onSendScreen { nextButton.assertIsEnabled() } + } + step("Click on 'Close' button") { + onSendScreen { closeButton.performClick() } + } + step("Assert 'Token Details' screen is displayed") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + } + } + + @AllureId("4762") + @DisplayName("Send (amount screen): switch equivalent") + @Test + fun switchEquivalentTest() { + val tokenName = "Ethereum" + val tokenAmount = "1" + val fiatAmount = "$2,535.63" + + setupHooks().run { + step("Open 'Send' screen") { + openSendScreen(tokenName) + } + step("Type '$tokenAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(tokenAmount) + } + } + step("Assert send amount = '$tokenAmount'") { + onSendScreen { amountInputTextField.assertTextContains(tokenAmount, substring = true) } + } + step("Assert equivalent amount = '$fiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount, substring = true) } + } + step("Click on 'Exchange' button") { + onSendScreen { exchangeIcon.performClick() } + } + step("Assert send amount = '$fiatAmount'") { + onSendScreen { amountInputTextField.assertTextContains(fiatAmount, substring = true) } + } + step("Assert equivalent amount = '$tokenAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(tokenAmount, substring = true) } + } + step("Click on 'Exchange' button") { + onSendScreen { exchangeIcon.performClick() } + } + step("Assert send amount = '$tokenAmount'") { + onSendScreen { amountInputTextField.assertTextContains(tokenAmount, substring = true) } + } + step("Assert equivalent amount = '$fiatAmount'") { + onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount, substring = true) } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index 0932e007ab..dbc955ab1e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -271,6 +271,7 @@ private fun AmountFieldError( style = TangemTheme.typography.caption2, color = color, textAlign = TextAlign.Center, + modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt index 49103a185d..928a782a86 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt @@ -5,6 +5,7 @@ object SendScreenTestTags { const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE" const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD" + const val AMOUNT_ERROR_TEXT = "SEND_SCREEN_AMOUNT_ERROR_TEXT" const val EQUIVALENT_INPUT_AMOUNT = "SEND_SCREEN_EQUIVALENT_INPUT_AMOUNT" const val EXCHANGE_ICON = "SEND_SCREEN_EXCHANGE_ICON" const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME" From d5cbaf22cf8c258bcab345bdb94d5d5e8c90054f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 09:48:23 +0500 Subject: [PATCH 057/130] Updated on 2026-08-14 --- .../utils/WalletWarningsSingleEventSender.kt | 2 +- .../domain/GetMultiWalletWarningsFactory.kt | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) 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 0813bc4d40..a51f2133e4 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 @@ -54,8 +54,8 @@ internal class WalletWarningsSingleEventSender @Inject constructor( is WalletNotification.FinishWalletActivation -> { if (event.type == WalletActivationBannerType.Warning && !isActivationBottomSheetShown.get()) { showFinishActivationBottomSheet(userWalletId) - isActivationBottomSheetShown.set(true) } + isActivationBottomSheetShown.set(true) } else -> Unit } 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 79df423642..f8d2fb389d 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 @@ -40,6 +40,7 @@ import com.tangem.hot.sdk.model.HotWalletId import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -67,7 +68,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -129,7 +130,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addFinishWalletActivationNotification( userWallet = userWallet, - totalFiatBalance = totalFiatBalance, + flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldAccessCodeSkipped = shouldAccessCodeSkipped, ) @@ -437,8 +438,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun TotalFiatBalance?.getFinishWalletActivationType(): WalletActivationBannerType { - return if ((this as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + private fun List?.getFinishWalletActivationType(): WalletActivationBannerType { + return if (this?.any { it.value.amount.orZero().isPositive() } == true) { WalletActivationBannerType.Warning } else { WalletActivationBannerType.Attention @@ -447,7 +448,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, - totalFiatBalance: Lce, + flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldAccessCodeSkipped: Boolean, ) { @@ -458,8 +459,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired - val type = totalFiatBalance.fold( - ifLoading = { it.getFinishWalletActivationType() }, + val type = flattenCurrencies.fold( + ifLoading = { return }, ifContent = { it.getFinishWalletActivationType() }, ifError = { WalletActivationBannerType.Attention }, ) From f5be772c91c843d53639e03834adef81c194c9bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 13:04:20 +0500 Subject: [PATCH 058/130] Updated on 2026-08-14 --- common/ui/detekt-baseline-debug.xml | 2 - data/nft/detekt-baseline-debug.xml | 1 - data/visa/detekt-baseline-debug.xml | 1 - data/wallet-connect/detekt-baseline-debug.xml | 1 - detekt_baseline_report.txt | 22 +++--- .../hot-wallet/impl/detekt-baseline-debug.xml | 2 - .../send-v2/api/detekt-baseline-debug.xml | 11 --- .../api/analytics/CommonSendAnalyticEvents.kt | 2 +- .../send/v2/api/entry/SendEntryRoute.kt | 2 +- .../CommonSendAmountAnalyticEvents.kt | 2 +- .../analytics/CommonSendFeeAnalyticEvents.kt | 2 +- .../feeSelector/entity/FeeSelectorData.kt | 2 +- .../send-v2/impl/detekt-baseline-debug.xml | 63 --------------- .../features/send/v2/common/ui/SendContent.kt | 14 ++-- .../features/send/v2/common/ui/TapHelp.kt | 6 +- .../send/v2/common/ui/state/ConfirmUM.kt | 2 +- .../DefaultSellRedirectDeepLinkHandler.kt | 4 +- .../DefaultSendEntryPointComponent.kt | 5 +- .../entrypoint/model/SendEntryPointModel.kt | 4 +- .../model/FeeSelectorAlertFactory.kt | 18 ++--- .../v2/feeselector/model/FeeSelectorModel.kt | 6 +- .../FeeSelectorCustomFieldConverter.kt | 6 +- ...eeSelectorCustomValueChangedTransformer.kt | 7 +- .../feeselector/ui/FeeSelectorBlockContent.kt | 13 ++-- .../ui/FeeSelectorModalBottomSheet.kt | 10 +-- .../v2/send/analytics/SendAnalyticEvents.kt | 6 +- .../v2/send/analytics/SendAnalyticHelper.kt | 6 +- .../v2/send/confirm/model/SendConfirmModel.kt | 24 +++--- .../SendConfirmInitialStateTransformer.kt | 2 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 2 +- .../features/send/v2/send/model/SendModel.kt | 10 +-- .../success/ui/SendConfirmSuccessContent.kt | 6 +- .../analytics/NFTSendAnalyticEvents.kt | 6 +- .../analytics/NFTSendAnalyticHelper.kt | 2 +- .../confirm/model/NFTSendConfirmModel.kt | 26 +++---- .../NFTSendConfirmInitialStateTransformer.kt | 2 +- .../confirm/ui/NFTSendConfirmContent.kt | 2 +- .../success/ui/NFTSendSuccessContent.kt | 6 +- .../amount/model/SendAmountModel.kt | 17 ++-- .../SendDestinationAnalyticEvents.kt | 2 +- .../destination/model/SendDestinationModel.kt | 20 ++--- .../SendRecipientHistoryListConverter.kt | 4 +- .../SendRecipientWalletListConverter.kt | 8 +- .../model/transformers/RecentListUtils.kt | 8 +- ...dDestinationValidationResultTransformer.kt | 4 +- .../destination/ui/SendDestinationContent.kt | 77 ++++++++++--------- .../bitcoin/BitcoinCustomFeeConverter.kt | 2 +- .../ethereum/EthereumCustomFeeConverter.kt | 28 +++++-- .../custom/kaspa/KaspaCustomFeeConverter.kt | 9 ++- .../analytics/NotificationsAnalyticEvents.kt | 2 +- ...firmationNotificationsTransformerV2Test.kt | 2 +- ...firmationNotificationsTransformerV2Test.kt | 2 +- .../model/WcSendTransactionModel.kt | 2 +- 53 files changed, 219 insertions(+), 276 deletions(-) delete mode 100644 features/send-v2/api/detekt-baseline-debug.xml delete mode 100644 features/send-v2/impl/detekt-baseline-debug.xml diff --git a/common/ui/detekt-baseline-debug.xml b/common/ui/detekt-baseline-debug.xml index 76624db4ad..13ac4c3486 100644 --- a/common/ui/detekt-baseline-debug.xml +++ b/common/ui/detekt-baseline-debug.xml @@ -18,9 +18,7 @@ CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal? CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent? MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -> { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -> { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -> null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) } - MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter${ createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) } MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) } - NamedArguments:TokenItemStateConverter.kt$TokenItemStateConverter$createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) NoNameShadowing:NavigationButtonsBlock.kt$navigationUM NoNameShadowing:UserWalletItem.kt$balance NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false diff --git a/data/nft/detekt-baseline-debug.xml b/data/nft/detekt-baseline-debug.xml index 08a3e3aba8..d4e81aea8d 100644 --- a/data/nft/detekt-baseline-debug.xml +++ b/data/nft/detekt-baseline-debug.xml @@ -3,7 +3,6 @@ MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = it ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ getNFTPersistenceStore(userWalletId, it).clear() getNFTRuntimeStore(userWalletId, it).clear() } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.id == collectionId) { it.changeAssetsStatusSource(source) } else { it } } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.identifier == sdkCollectionId) { it.copy(assets = assets) } else { it } } diff --git a/data/visa/detekt-baseline-debug.xml b/data/visa/detekt-baseline-debug.xml index 96bb302f53..d9da47d03d 100644 --- a/data/visa/detekt-baseline-debug.xml +++ b/data/visa/detekt-baseline-debug.xml @@ -14,7 +14,6 @@ NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (config != null) return@withLock requireNotNull(config) NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (provider != null) return@withLock requireNotNull(provider) NullableToStringCall:DefaultOnboardingRepository.kt$DefaultOnboardingRepository$${error.message} - NullableToStringCall:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$${error.message} RedundantSuspendModifier:DefaultVisaRepository.kt$DefaultVisaRepository$suspend SuspendFunSwallowedCancellation:DefaultVisaRepository.kt$DefaultVisaRepository$runCatching SuspendFunSwallowedCancellation:VisaApiRequestMaker.kt$VisaApiRequestMaker$runCatching diff --git a/data/wallet-connect/detekt-baseline-debug.xml b/data/wallet-connect/detekt-baseline-debug.xml index 6d06073a70..f357361c71 100644 --- a/data/wallet-connect/detekt-baseline-debug.xml +++ b/data/wallet-connect/detekt-baseline-debug.xml @@ -47,7 +47,6 @@ SuspendFunSwallowedCancellation:DefaultWcPairUseCase.kt$DefaultWcPairUseCase$runCatching UseAnyOrNoneInsteadOfFind:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$find { it.sdkModel.topic == dto.topic } UseEmptyCounterpart:AssociateNetworksDelegate.kt$AssociateNetworksDelegate.Companion$listOf() - UseEmptyCounterpart:DefaultWcRequestService.kt$DefaultWcRequestService$setOf() UseEmptyCounterpart:WcAppMetaDataConverter.kt$WcAppMetaDataConverter$listOf() UseEmptyCounterpart:WcNetworksConverter.kt$WcNetworksConverter$listOf() UseEmptyCounterpart:WcSdkSessionConverter.kt$WcSdkSessionConverter$listOf() diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index aed69f1e1f..d2f8545c7e 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-04 17:37:53 +Date: 2025-12-08 12:25:24 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1380 - Modules with Issues: 50 + Total Issues: 1311 + Modules with Issues: 48 Average Issues per Module: 27 Progress: - Fixed: 422 out of 1802 (23%) - Remaining: 1380 + Fixed: 491 out of 1802 (27%) + Remaining: 1311 ========================================== All Modules with Issues (sorted by count) @@ -35,9 +35,8 @@ features/markets/impl 148 features/wallet/impl 145 features/onboarding-v2/impl 130 features/swap/impl 67 -features/send-v2/impl 57 -data/wallet-connect 55 -features/hot-wallet/impl 53 +data/wallet-connect 54 +features/hot-wallet/impl 51 features/walletconnect/impl 47 features/tokendetails/impl 46 features/staking/impl 46 @@ -47,13 +46,13 @@ domain/wallets 37 features/nft/impl 34 features/tester/impl 30 core/ui 27 -common/ui 26 features/swap/domain 25 domain/tokens 25 -data/visa 22 +common/ui 24 features/yield-supply/impl 21 features/tangempay/details/impl 21 -data/nft 20 +data/visa 21 +data/nft 19 features/swap/data 15 data/wallets 13 data/swap 13 @@ -73,7 +72,6 @@ libs/tangem-sdk-api 7 data/txhistory 7 features/welcome/impl 6 libs/visa 5 -features/send-v2/api 5 features/home/impl 5 domain/markets 5 data/account 5 diff --git a/features/hot-wallet/impl/detekt-baseline-debug.xml b/features/hot-wallet/impl/detekt-baseline-debug.xml index f34cb27c7f..2e3bf90a57 100644 --- a/features/hot-wallet/impl/detekt-baseline-debug.xml +++ b/features/hot-wallet/impl/detekt-baseline-debug.xml @@ -11,7 +11,6 @@ BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val secondCheckboxChecked: Boolean BooleanPropertyNaming:HotAccessCodeRequestUM.kt$HotAccessCodeRequestUM$val useBiometricVisible: Boolean = true BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showBackButton: Boolean - BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showFeedbackButton: Boolean BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showSkipButton: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonEnabled: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean @@ -51,7 +50,6 @@ PropertyUsedBeforeDeclaration:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$uiState ReusedModifierInstance:AddExistingWalletImportContent.kt$OutlineTextFieldWithIcon( modifier = modifier .padding(horizontal = 16.dp) .fillMaxWidth(), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, label = stringResourceSafe(id = R.string.common_passphrase), placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = state.onPassphraseInfoClick, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, keyboardType = KeyboardType.Password, ), ) ReusedModifierInstance:HotAccessCodeRequestFullScreenContent.kt$AnimatedVisibility( modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), ) { Column( Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), horizontalAlignment = Alignment.CenterHorizontally, ) { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onDismiss), ) SpacerH(68.dp) Column( Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), text = stringResourceSafe(R.string.access_code_check_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH24() PinTextField( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), length = 6, isPasswordVisual = true, value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) SpacerH(20.dp) AnimatedVisibility( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), visible = state.wrongAccessCodeText != null, enter = fadeIn(), exit = fadeOut(), ) { val wrongAccessCodeText = state.wrongAccessCodeText ?: return@AnimatedVisibility Text( text = wrongAccessCodeText.resolveReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2.copy( lineBreak = LineBreak.Heading, ), color = TangemTheme.colors.text.warning, ) } } AnimatedVisibility( visible = state.useBiometricVisible, enter = fadeIn(), exit = fadeOut(), ) { SecondaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth() .navigationBarsPadding() .imePadding(), text = stringResourceSafe( id = R.string.welcome_unlock, stringResourceSafe(R.string.common_biometrics), ), onClick = state.useBiometricClick, ) } } } - ReusedModifierInstance:HotWalletStepper.kt$TangemTopAppBar( startButton = if (state.showBackButton) { TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { 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 }, title = state.title, containerColor = TangemTheme.colors.background.primary, modifier = modifier, titleAlignment = Alignment.CenterHorizontally, ) SuspendFunSwallowedCancellation:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$runCatching SuspendFunSwallowedCancellation:ManualBackupCheckModel.kt$ManualBackupCheckModel$runCatching SuspendFunSwallowedCancellation:ManualBackupPhraseModel.kt$ManualBackupPhraseModel$runCatching diff --git a/features/send-v2/api/detekt-baseline-debug.xml b/features/send-v2/api/detekt-baseline-debug.xml deleted file mode 100644 index 31859862c5..0000000000 --- a/features/send-v2/api/detekt-baseline-debug.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - BooleanPropertyNaming:FeeSelectorData.kt$FeeSelectorData$val removeSuggestedFee: Boolean = false - BooleanPropertyNaming:SendEntryRoute.kt$SendEntryRoute.ChooseToken$val showSendViaSwapNotification: Boolean - UseEmptyCounterpart:CommonSendAmountAnalyticEvents.kt$CommonSendAmountAnalyticEvents$mapOf() - UseEmptyCounterpart:CommonSendAnalyticEvents.kt$CommonSendAnalyticEvents$mapOf() - UseEmptyCounterpart:CommonSendFeeAnalyticEvents.kt$CommonSendFeeAnalyticEvents$mapOf() - - diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 799c657659..cb58f30134 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -12,7 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class CommonSendAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { /** Recipient address screen opened */ diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt index 2fab40cdc0..ae701c2f99 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt @@ -13,6 +13,6 @@ sealed class SendEntryRoute : Route { data object SendWithSwap : SendEntryRoute() /** Route to choose token screen for send via swap */ data class ChooseToken( - val showSendViaSwapNotification: Boolean, + val isShowSendViaSwapNotification: Boolean, ) : SendEntryRoute() } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt index 5cc038b0cb..cfc6409387 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt @@ -10,7 +10,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents sealed class CommonSendAmountAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { /** Selected currency */ diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt index 500e8b4f69..7294b5e0b8 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt @@ -9,7 +9,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.Common sealed class CommonSendFeeAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt index 66fa5e4156..be6f6de2bb 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt @@ -1,5 +1,5 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.entity data class FeeSelectorData( - val removeSuggestedFee: Boolean = false, + val isRemoveSuggestedFee: Boolean = false, ) \ No newline at end of file diff --git a/features/send-v2/impl/detekt-baseline-debug.xml b/features/send-v2/impl/detekt-baseline-debug.xml deleted file mode 100644 index 5c064c3c41..0000000000 --- a/features/send-v2/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - BooleanPropertyNaming:ConfirmUM.kt$ConfirmUM.Content$val showTapHelp: Boolean - BooleanPropertyNaming:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) - BooleanPropertyNaming:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) - BooleanPropertyNaming:FeeSelectorModalBottomSheet.kt$val lastItem = index == state.feeItems.size - 1 - BooleanPropertyNaming:FeeSelectorModalBottomSheet.kt$val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce - BooleanPropertyNaming:NFTSendAnalyticEvents.kt$NFTSendAnalyticEvents.TransactionScreenOpened$val nonceNotEmpty: Boolean - BooleanPropertyNaming:NFTSendSuccessContent.kt$var visible by remember { mutableStateOf(false) } - BooleanPropertyNaming:SendAnalyticEvents.kt$SendAnalyticEvents.TransactionScreenOpened$val nonceNotEmpty: Boolean - BooleanPropertyNaming:SendAnalyticHelper.kt$SendAnalyticHelper$val blockchainAddressForEns = (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.isAddressEns - BooleanPropertyNaming:SendConfirmSuccessContent.kt$var visible by remember { mutableStateOf(false) } - BooleanPropertyNaming:SendEntryPointModel.kt$SendEntryPointModel$val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - BooleanPropertyNaming:SendRecipientHistoryListConverter.kt$SendRecipientHistoryListConverter$val notZero = !item.amount.isZero() - BooleanPropertyNaming:TapHelp.kt$var wrappedIsDisplay by remember { mutableStateOf(false) } - CanBeNonNullable:SendDestinationContent.kt$memoField: DestinationTextFieldUM.RecipientMemo? - CanBeNonNullable:SendDestinationModel.kt$SendDestinationModel$type: EnterAddressSource? - MaxChainedCallsOnSameLine:FeeSelectorBlockContent.kt$state.selectedFeeItem.fee.amount.value.format { crypto( symbol = state.selectedFeeItem.fee.amount.currencySymbol, decimals = state.selectedFeeItem.fee.amount.decimals, ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) } - MultilineLambdaItParameter:DefaultSellRedirectDeepLinkHandler.kt$DefaultSellRedirectDeepLinkHandler${ Timber.e("Error on getting cryptoCurrency: $it") return@launch } - MultilineLambdaItParameter:FeeSelectorCustomFieldConverter.kt$FeeSelectorCustomFieldConverter${ when (it) { is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( minimumFee = it, customValues = customValues, ) else -> customValues } } - MultilineLambdaItParameter:FeeSelectorModel.kt$FeeSelectorModel${ feeSelectorAlertFactory.getFeeUpdatedAlert( newFee = it, feeSelectorUM = uiState.value, proceedAction = { modelScope.launch { feeSelectorCheckReloadTrigger.callbackCheckResult(true) } }, stopAction = { modelScope.launch { feeSelectorCheckReloadTrigger.callbackCheckResult(false) } }, ) } - MultilineLambdaItParameter:KaspaCustomFeeConverter.kt$KaspaCustomFeeConverter${ 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, ), ), ) } } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ it.copy( confirmUM = NFTSendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ it.copy( confirmUM = NFTSendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = params.appCurrency, analyticsCategoryName = analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ val isFeeNotNull = it.feeSelectorUM is FeeSelectorUMRedesigned.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && isFeeNotNull, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:RecentListUtils.kt${ add( DestinationRecipientListUM( id = "$tag$it", isLoading = false, isVisible = false, ), ) } - MultilineLambdaItParameter:RecentListUtils.kt${ add( DestinationRecipientListUM( id = "$tag$it", isLoading = true, ), ) } - MultilineLambdaItParameter:SendAmountModel.kt$SendAmountModel${ (it as? AmountState.Data)?.copy( isPrimaryButtonEnabled = false, ) ?: it } - MultilineLambdaItParameter:SendAmountModel.kt$SendAmountModel${ EnterAmountBoundary( amount = it, fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ it.copy( confirmUM = SendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), confirmData = confirmData, ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ it.copy( confirmUM = SendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, amountUM = uiState.value.amountUM, analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = appCurrency, analyticsCategoryName = params.analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:SendDestinationContent.kt${ if (it) { content() } else { Box(modifier = Modifier.fillMaxWidth()) } } - MultilineLambdaItParameter:SendDestinationModel.kt$SendDestinationModel${ analyticsEventHandler.send( SendDestinationAnalyticEvents.AddressEntered( categoryName = analyticsCategoryName, source = params.analyticsSendSource, method = it, isValid = addressValidationResult.isRight(), ), ) } - MultilineLambdaItParameter:SendDestinationModel.kt$SendDestinationModel${ if (it.network.rawId == cryptoCurrencyNetwork.rawId) { getNetworkAddressesUseCase.invokeSync( userWalletId = wallet.walletId, networkRawId = it.network.id.rawId, ) } else { null } } - MultilineLambdaItParameter:SendDestinationValidationResultTransformer.kt$SendDestinationValidationResultTransformer${ when (it) { is AddressValidation.Error.DataError, AddressValidation.Error.InvalidAddress, -> R.string.send_recipient_address_error AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet } } - MultilineLambdaItParameter:SendModel.kt$SendModel${ Timber.w(it.toString()) showAlertError() return@launch } - MultilineLambdaItParameter:SendModel.kt$SendModel${ it.copy( destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, ).transform(DestinationUM.Empty()), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, confirmData = null, navigationUM = NavigationUM.Empty, ) } - MultilineLambdaItParameter:SendRecipientWalletListConverter.kt$SendRecipientWalletListConverter${ val isCoin = it.cryptoCurrency is CryptoCurrency.Coin val isNotSameAddress = it.address != senderAddress val isNotBlankAddress = it.address.isNotBlank() isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) } - NamedArguments:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$onValueChange(feeValue, customValues, index, value) - NamedArguments:FeeSelectorCustomValueChangedTransformer.kt$FeeSelectorCustomValueChangedTransformer$onValueChange(state, customFee.customValues, index, value) - 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 - 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() - ReusedModifierInstance:DefaultSendEntryPointComponent.kt$DefaultSendEntryPointComponent$Content(modifier.fillMaxSize()) - UnnecessaryLet:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$let(::add) - UnnecessaryLet:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$let(::addAll) - UnnecessaryLet:SendAmountModel.kt$SendAmountModel$let { onCurrencyChangeClick(isEnterInFiat) } - UseEmptyCounterpart:NFTSendAnalyticEvents.kt$NFTSendAnalyticEvents$mapOf() - UseEmptyCounterpart:NotificationsAnalyticEvents.kt$NotificationsAnalyticEvents$mapOf() - UseEmptyCounterpart:SendAnalyticEvents.kt$SendAnalyticEvents$mapOf() - UseEmptyCounterpart:SendDestinationAnalyticEvents.kt$SendDestinationAnalyticEvents$mapOf() - VarCouldBeVal:SendDestinationModel.kt$SendDestinationModel$private var validationJobHolder = JobHolder() - VarCouldBeVal:SendModel.kt$SendModel$private var balanceHidingJobHolder = JobHolder() - - diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index cd6bfb3235..43924cbf0e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -85,14 +85,14 @@ internal fun SendContent( @Composable private fun SendAppBar(navigationUM: NavigationUM) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return + val navigationUMContent = navigationUM as? NavigationUM.Content ?: return AppBarWithBackButtonAndIcon( - text = navigationUM.title.resolveReference(), - subtitle = navigationUM.subtitle?.resolveReference(), - onBackClick = navigationUM.backIconClick, - onIconClick = navigationUM.additionalIconClick, - backIconRes = navigationUM.backIconRes, - iconRes = navigationUM.additionalIconRes, + text = navigationUMContent.title.resolveReference(), + subtitle = navigationUMContent.subtitle?.resolveReference(), + onBackClick = navigationUMContent.backIconClick, + onIconClick = navigationUMContent.additionalIconClick, + backIconRes = navigationUMContent.backIconRes, + iconRes = navigationUMContent.additionalIconRes, backgroundColor = TangemTheme.colors.background.tertiary, modifier = Modifier.height(TangemTheme.dimens.size56), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt index 480650ee0e..43251bec2f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt @@ -22,14 +22,14 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L internal fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { item(key = TAP_HELP_KEY) { - var wrappedIsDisplay by remember { mutableStateOf(false) } + var isWrappedDisplay by remember { mutableStateOf(false) } LaunchedEffect(key1 = isDisplay) { delay(TAP_HELP_ANIMATION_DELAY) - wrappedIsDisplay = isDisplay + isWrappedDisplay = isDisplay } - if (wrappedIsDisplay) { + if (isWrappedDisplay) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt index 78cc00c705..7a63af4176 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt @@ -14,7 +14,7 @@ internal sealed class ConfirmUM { override val isPrimaryButtonEnabled: Boolean = false, val walletName: TextReference, val isSending: Boolean, - val showTapHelp: Boolean, + val isShowTapHelp: Boolean, val sendingFooter: TextReference, val notifications: ImmutableList, ) : ConfirmUM() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt index 825518011b..402cd981c5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt @@ -51,8 +51,8 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( } scope.launch { - val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, currencyId).getOrElse { - Timber.e("Error on getting cryptoCurrency: $it") + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, currencyId).getOrElse { error -> + Timber.e("Error on getting cryptoCurrency: $error") return@launch } // Convert using universal parser to account for regional separators diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index b9f865983a..9e41df2a03 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -106,6 +106,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( Children( stack = childStackValue, + modifier = modifier, animation = stackAnimation { child -> when (child.configuration) { SendEntryRoute.Send, @@ -115,7 +116,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( } }, ) { child -> - child.instance.Content(modifier.fillMaxSize()) + child.instance.Content(Modifier.fillMaxSize()) } } @@ -125,7 +126,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( ): ComposableContentComponent = when (configuration) { is SendEntryRoute.ChooseToken -> getManagedTokensComponent( componentContext = factoryContext, - showSendViaSwapNotification = configuration.showSendViaSwapNotification, + showSendViaSwapNotification = configuration.isShowSendViaSwapNotification, ) SendEntryRoute.Send -> sendComponent SendEntryRoute.SendWithSwap -> sendWithSwapComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 02ba094907..f3ff4ee117 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -42,12 +42,12 @@ internal class SendEntryPointModel @Inject constructor( lastSavedAmount = lastAmount isEnterInFiat = isEnterInFiatSelected modelScope.launch { - val showSendViaSwapNotification = shouldShowNotificationUseCase( + val isShowSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) router.push( SendEntryRoute.ChooseToken( - showSendViaSwapNotification = showSendViaSwapNotification, + isShowSendViaSwapNotification = isShowSendViaSwapNotification, ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt index 43091d08a1..6abebb0fb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt @@ -20,10 +20,10 @@ internal class FeeSelectorAlertFactory @Inject constructor( ) { fun checkAndShowAlerts(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit) { - val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) - val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) + val isShowFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) + val isShowFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) - if (!showFeeTooLow && !showFeeTooHigh) { + if (!isShowFeeTooLow && !isShowFeeTooHigh) { onConfirmClick() } } @@ -78,20 +78,20 @@ internal class FeeSelectorAlertFactory @Inject constructor( } fun getFeeUpdatedAlert( - newFee: TransactionFee, + newTransactionFee: TransactionFee, feeSelectorUM: FeeSelectorUM, proceedAction: () -> Unit, stopAction: () -> Unit, ) { if (feeSelectorUM !is FeeSelectorUM.Content) return - val newFee = when (newFee) { - is TransactionFee.Single -> newFee.normal + val newFee = when (newTransactionFee) { + is TransactionFee.Single -> newTransactionFee.normal is TransactionFee.Choosable -> { when (feeSelectorUM.selectedFeeItem) { is FeeItem.Suggested -> feeSelectorUM.selectedFeeItem.fee - is FeeItem.Slow -> newFee.minimum - is FeeItem.Market -> newFee.normal - is FeeItem.Fast -> newFee.priority + is FeeItem.Slow -> newTransactionFee.minimum + is FeeItem.Market -> newTransactionFee.normal + is FeeItem.Fast -> newTransactionFee.priority is FeeItem.Custom -> return } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index ba91d7d75e..c290cc9fb3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -197,7 +197,7 @@ internal class FeeSelectorModel @Inject constructor( private fun subscribeOnFeeReloadTriggerUpdates() { feeSelectorReloadListener.reloadTriggerFlow .onEach { data -> - if (data.removeSuggestedFee) { + if (data.isRemoveSuggestedFee) { uiState.update(FeeSelectorRemoveSuggestedTransformer) } loadFee() @@ -220,9 +220,9 @@ internal class FeeSelectorModel @Inject constructor( private fun checkLoadFee() { modelScope.launch { params.onLoadFee().fold( - ifRight = { + ifRight = { newFee -> feeSelectorAlertFactory.getFeeUpdatedAlert( - newFee = it, + newTransactionFee = newFee, feeSelectorUM = uiState.value, proceedAction = { modelScope.launch { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt index 258312e8a0..344bb80387 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -107,10 +107,10 @@ internal class FeeSelectorCustomFieldConverter( when (val fees = feeSelectorState.fees) { is TransactionFee.Choosable -> fees.minimum is TransactionFee.Single -> fees.normal - }.let { - when (it) { + }.let { fee -> + when (fee) { is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( - minimumFee = it, + minimumFee = fee, customValues = customValues, ) else -> customValues diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt index 9418304d3b..5e94e5a7c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt @@ -27,7 +27,12 @@ internal class FeeSelectorCustomValueChangedTransformer( feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, normalFee = state.selectedFeeItem.fee, ) - val updatedCustomValues = customFeeConverter.onValueChange(state, customFee.customValues, index, value) + val updatedCustomValues = customFeeConverter.onValueChange( + feeSelectorState = state, + customValues = customFee.customValues, + index = index, + value = value, + ) val newCustomFee = customFee.copy( fee = customFeeConverter.convertBack(updatedCustomValues), customValues = updatedCustomValues, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index 30dcb3c687..5eb28eda97 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -172,12 +172,13 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi approximate = state.feeExtraInfo.isFeeApproximate, ) } else { - state.selectedFeeItem.fee.amount.value.format { - crypto( - symbol = state.selectedFeeItem.fee.amount.currencySymbol, - decimals = state.selectedFeeItem.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - } + state.selectedFeeItem.fee.amount.value + .format { + crypto( + symbol = state.selectedFeeItem.fee.amount.currencySymbol, + decimals = state.selectedFeeItem.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + } }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 0027bda872..daa164aa95 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -129,7 +129,7 @@ private fun FeeSelectorItems( val feeFiatRateUM = state.feeFiatRateUM state.feeItems.fastForEachIndexed { index, item -> val isSelected = item.isSameClass(state.selectedFeeItem) - val lastItem = index == state.feeItems.size - 1 + val isLastItem = index == state.feeItems.size - 1 val iconTint by animateColorAsState( targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, label = "Fee selector icon tint change", @@ -180,7 +180,7 @@ private fun FeeSelectorItems( null }, ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, + showDivider = !isSelected && !isLastItem, ) } } @@ -243,7 +243,7 @@ private fun ExpandedCustomFeeItems( ) { Column(modifier = modifier) { customFeeFields.fastForEachIndexed { index, field -> - val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce + val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce if (field.label != null) { InputRowEnterInfoAmountV2( text = field.value, @@ -256,7 +256,7 @@ private fun ExpandedCustomFeeItems( keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, onValueChange = { onValueChange(index, it) }, - showDivider = showDivider, + showDivider = isShowDivider, isReadOnly = field.isReadonly, ) } else { @@ -270,7 +270,7 @@ private fun ExpandedCustomFeeItems( onValueChange = { onValueChange(index, it) }, keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, - showDivider = showDivider, + showDivider = isShowDivider, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 5774e3b776..bf26de5704 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -15,7 +15,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents */ internal sealed class SendAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { /** Transaction send screen opened */ @@ -23,7 +23,7 @@ internal sealed class SendAnalyticEvents( val token: String, val feeType: AnalyticsParam.FeeType, val blockchain: String, - val nonceNotEmpty: Boolean, + val isNonceNotEmpty: Boolean, private val ensStatus: AnalyticsParam.EmptyFull, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", @@ -31,7 +31,7 @@ internal sealed class SendAnalyticEvents( TOKEN_PARAM to token, FEE_TYPE to feeType.value, BLOCKCHAIN to blockchain, - NONCE to nonceNotEmpty.toString().capitalize(), + NONCE to isNonceNotEmpty.toString().capitalize(), ENS_ADDRESS to when (ensStatus) { AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() AnalyticsParam.EmptyFull.Full -> true.toString().capitalize() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 48f87023ed..4d56a5075f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -26,7 +26,7 @@ internal class SendAnalyticHelper @Inject constructor( token = cryptoCurrency.symbol, feeType = feeType, blockchain = cryptoCurrency.network.name, - nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, + isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), ), ) @@ -54,9 +54,9 @@ internal class SendAnalyticHelper @Inject constructor( } private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EmptyFull { - val blockchainAddressForEns = + val isBlockchainAddressForEns = (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.isAddressEns - return if (blockchainAddressForEns == true) { + return if (isBlockchainAddressForEns == true) { AnalyticsParam.EmptyFull.Full } else { AnalyticsParam.EmptyFull.Empty diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 5c00ee1e19..d36c18e827 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -306,8 +306,8 @@ internal class SendConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty) { - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = SendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), @@ -323,9 +323,9 @@ internal class SendConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - _uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + _uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -333,12 +333,12 @@ internal class SendConfirmModel @Inject constructor( private fun subscribeOnNotificationsUpdateTrigger() { notificationsUpdateListener.hasErrorFlow .onEach { hasError -> - _uiState.update { - val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + _uiState.update { state -> + val feeUM = state.feeSelectorUM as? FeeSelectorUMRedesigned.Content + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } } @@ -495,8 +495,8 @@ internal class SendConfirmModel @Inject constructor( feeError = confirmData.feeError, ), ) - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = SendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, amountUM = uiState.value.amountUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt index 1231a4954e..bd50b38485 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt @@ -13,7 +13,7 @@ internal class SendConfirmInitialStateTransformer( return ConfirmUM.Content( walletName = walletName, isSending = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index d617e88182..cb97fc4632 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -52,7 +52,7 @@ internal fun SendConfirmContent( feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { - tapHelp(isDisplay = confirmUM.showTapHelp) + tapHelp(isDisplay = confirmUM.isShowTapHelp) with(notificationsComponent) { content( state = notificationsUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 38c5c33abd..1028a207b5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -147,7 +147,7 @@ internal class SendModel @Inject constructor( var appCurrency: AppCurrency = AppCurrency.Default var predefinedValues: PredefinedValues = PredefinedValues.Empty - private var balanceHidingJobHolder = JobHolder() + private val balanceHidingJobHolder = JobHolder() init { subscribeOnBalanceHidden() @@ -226,8 +226,8 @@ internal class SendModel @Inject constructor( } override fun resetSendNavigation() { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, ).transform(DestinationUM.Empty()), @@ -358,8 +358,8 @@ internal class SendModel @Inject constructor( ) } }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + Timber.w(error.toString()) showAlertError() return@launch }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index fbd5a692d6..7d07f291bd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -29,17 +29,17 @@ import kotlinx.coroutines.delay @Composable internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { - var visible by remember { mutableStateOf(false) } + var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(ANIMATION_DELAY) - visible = true + isVisible = true } val height = ANIMATION_OFFSET.toPx().toInt() AnimatedVisibility( - visible = visible, + visible = isVisible, enter = slideInVertically( initialOffsetY = { height }, ).plus(fadeIn()), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt index f542b5f217..25700edaa6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt @@ -14,7 +14,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents */ internal sealed class NFTSendAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.NFT_SEND_CATEGORY, event = event, params = params) { /** Transaction send screen opened */ @@ -22,14 +22,14 @@ internal sealed class NFTSendAnalyticEvents( val token: String, val feeType: AnalyticsParam.FeeType, val blockchain: String, - val nonceNotEmpty: Boolean, + val isNonceNotEmpty: Boolean, ) : NFTSendAnalyticEvents( event = "NFT Sent Screen Opened", params = mapOf( TOKEN_PARAM to token, FEE_TYPE to feeType.value, BLOCKCHAIN to blockchain, - NONCE to nonceNotEmpty.toString().capitalize(), + NONCE to isNonceNotEmpty.toString().capitalize(), ), ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index c25e1fc99a..bab316c20f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -27,7 +27,7 @@ internal class NFTSendAnalyticHelper @Inject constructor( token = cryptoCurrency.symbol, feeType = feeType, blockchain = cryptoCurrency.network.name, - nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, + isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ), ) analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 1c7c92bc7e..8bab99a2a4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -195,7 +195,7 @@ internal class NFTSendConfirmModel @Inject constructor( derivationPath = cryptoCurrency.network.derivationPath.value, destinationAddress = confirmData.enteredDestination.orEmpty(), tokenSymbol = null, - amount = params.nftAsset.amount.toString(), + amount = params.nftAsset.amount?.toString().orEmpty(), fee = confirmData.fee?.amount?.value?.stripZeroPlainString(), ), ) @@ -213,8 +213,8 @@ internal class NFTSendConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty || isEmptyFee) { - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = NFTSendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), @@ -229,9 +229,9 @@ internal class NFTSendConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - _uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + _uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -239,13 +239,13 @@ internal class NFTSendConfirmModel @Inject constructor( private fun subscribeOnNotificationsUpdateTrigger() { notificationsUpdateListener.hasErrorFlow .onEach { hasError -> - _uiState.update { - val isFeeNotNull = it.feeSelectorUM is FeeSelectorUMRedesigned.Content + _uiState.update { state -> + val isFeeNotNull = state.feeSelectorUM is FeeSelectorUMRedesigned.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && isFeeNotNull, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } } @@ -353,8 +353,8 @@ internal class NFTSendConfirmModel @Inject constructor( ), ) } - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = NFTSendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, analyticsEventHandler = analyticsEventHandler, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt index e7f383dbe3..de24a6a022 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt @@ -12,7 +12,7 @@ internal class NFTSendConfirmInitialStateTransformer( override fun transform(prevState: ConfirmUM): ConfirmUM { return ConfirmUM.Content( isSending = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, walletName = walletName, notifications = persistentListOf(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 3f93769f0e..c950a991ae 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -45,7 +45,7 @@ internal fun NFTSendConfirmContent( feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { - tapHelp(isDisplay = confirmUM.showTapHelp) + tapHelp(isDisplay = confirmUM.isShowTapHelp) with(notificationsComponent) { content( state = notificationsUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt index c90b4429e3..b5e13f0e06 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -34,17 +34,17 @@ internal fun NFTSendSuccessContent( nftDetailsBlockComponent: NFTDetailsBlockComponent, modifier: Modifier = Modifier, ) { - var visible by remember { mutableStateOf(false) } + var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(ANIMATION_DELAY) - visible = true + isVisible = true } val height = ANIMATION_OFFSET.toPx().toInt() AnimatedVisibility( - visible = visible, + visible = isVisible, enter = slideInVertically( initialOffsetY = { height }, ).plus(fadeIn()), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 9be514d748..b232d081f9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -155,9 +155,9 @@ internal class SendAmountModel @Inject constructor( minAmountBoundary = getMinimumTransactionAmountSyncUseCase( userWalletId = params.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull()?.let { + ).getOrNull()?.let { amount -> EnterAmountBoundary( - amount = it, + amount = amount, fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), ) } @@ -183,6 +183,7 @@ internal class SendAmountModel @Inject constructor( account: Account.CryptoPortfolio?, isAccountsMode: Boolean, ) { + val userWallet = userWallet if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 val walletTitle = if (isOnlyOneWallet) { @@ -190,7 +191,7 @@ internal class SendAmountModel @Inject constructor( } else { resourceReference( R.string.send_from_wallet_name, - WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY] + WrappedList(listOf(userWallet.name)), // TODO [REDACTED_TASK_KEY] ) } _uiState.update { @@ -297,10 +298,10 @@ internal class SendAmountModel @Inject constructor( private fun confirmConvertToToken() { val amountParams = params as? SendAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value as? AmountState.Data - _uiState.update { - (it as? AmountState.Data)?.copy( + _uiState.update { state -> + (state as? AmountState.Data)?.copy( isPrimaryButtonEnabled = false, - ) ?: it + ) ?: state } val isEnterInFiatSelected = amountFieldData?.amountTextField?.isFiatValue == true @@ -352,7 +353,9 @@ internal class SendAmountModel @Inject constructor( private fun subscribeOnAmountUpdateTriggerUpdates() { sendAmountUpdateListener.updateAmountTriggerFlow .onEach { (amount, isEnterInFiat) -> - isEnterInFiat?.let { onCurrencyChangeClick(isEnterInFiat) } + if (isEnterInFiat != null) { + onCurrencyChangeClick(isEnterInFiat) + } onAmountValueChange(amount) saveResult() }.launchIn(modelScope) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt index 39191837d0..2907e373d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt @@ -8,7 +8,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents internal sealed class SendDestinationAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 5870afd0fb..ba2ab8f1b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -85,7 +85,7 @@ internal class SendDestinationModel @Inject constructor( private val senderAddresses = MutableStateFlow>(emptyList()) - private var validationJobHolder = JobHolder() + private val validationJobHolder = JobHolder() init { configDestinationNavigation() @@ -234,11 +234,11 @@ internal class SendDestinationModel @Inject constructor( .map { wallet -> async { val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { - if (it.network.rawId == cryptoCurrencyNetwork.rawId) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { cryptoCurrency -> + if (cryptoCurrency.network.rawId == cryptoCurrencyNetwork.rawId) { getNetworkAddressesUseCase.invokeSync( userWalletId = wallet.walletId, - networkRawId = it.network.id.rawId, + networkRawId = cryptoCurrency.network.id.rawId, ) } else { null @@ -323,12 +323,12 @@ internal class SendDestinationModel @Inject constructor( network = cryptoCurrency.network, ) - type?.let { + if (type != null) { analyticsEventHandler.send( SendDestinationAnalyticEvents.AddressEntered( categoryName = analyticsCategoryName, source = params.analyticsSendSource, - method = it, + method = type, isValid = addressValidationResult.isRight(), ), ) @@ -339,12 +339,14 @@ internal class SendDestinationModel @Inject constructor( memoValidationResult, ), ) - autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + if (type != null) { + autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + } }.saveIn(validationJobHolder) } - private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - if (type?.isAutoNext == true && isValidAddress && isValidMemo) { + private fun autoNextFromRecipient(type: EnterAddressSource, isValidAddress: Boolean, isValidMemo: Boolean) { + if (type.isAutoNext && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt index 1b9d4bcbc9..6b9979cbfa 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt @@ -39,8 +39,8 @@ internal class SendRecipientHistoryListConverter( } else { item.sourceType is TxInfo.SourceType.Single } - val notZero = !item.amount.isZero() - isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero + val isNotZero = !item.amount.isZero() + isTransfer && isSingleAddress && isNotContract && item.isOutgoing && isNotZero } .take(RECENT_LIST_SIZE) .mapIndexed { index, tx -> diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index 1e43755cbc..94223cd153 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -30,10 +30,10 @@ internal class SendRecipientWalletListConverter( var walletsCounter = 0 return this.filterNotNull() - .filter { - val isCoin = it.cryptoCurrency is CryptoCurrency.Coin - val isNotSameAddress = it.address != senderAddress - val isNotBlankAddress = it.address.isNotBlank() + .filter { destinationWallet -> + val isCoin = destinationWallet.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = destinationWallet.address != senderAddress + val isNotBlankAddress = destinationWallet.address.isNotBlank() isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt index ad2e643f1e..638d7abede 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt @@ -9,10 +9,10 @@ internal const val WALLET_KEY_TAG = "wallet" internal const val RECENT_KEY_TAG = "recent" internal fun loadingListState(tag: String, count: Int) = buildList { - repeat(count) { + repeat(count) { i -> add( DestinationRecipientListUM( - id = "$tag$it", + id = "$tag$i", isLoading = true, ), ) @@ -20,10 +20,10 @@ internal fun loadingListState(tag: String, count: Int) = buildList { }.toPersistentList() internal fun emptyListState(tag: String, count: Int) = buildList { - repeat(count) { + repeat(count) { i -> add( DestinationRecipientListUM( - id = "$tag$it", + id = "$tag$i", isLoading = false, isVisible = false, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index da3d010f3f..35cae2c471 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -21,8 +21,8 @@ internal class SendDestinationValidationResultTransformer( val isValidAddress = addressValidationResult.isRight() val isValidMemo = memoValidationResult.isRight() - val addressErrorText = addressValidationResult.mapLeft { - when (it) { + val addressErrorText = addressValidationResult.mapLeft { error -> + when (error) { is AddressValidation.Error.DataError, AddressValidation.Error.InvalidAddress, -> R.string.send_recipient_address_error diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 96592fdf28..4c18c81642 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -67,10 +67,13 @@ internal fun SendDestinationContent( onAddressChange = clickIntents::onRecipientAddressValueChange, onQrCodeClick = clickIntents::onQrCodeScanClick, ) - memoField( - memoField = state.memoTextField, - onMemoChange = clickIntents::onRecipientMemoValueChange, - ) + val memoTextField = state.memoTextField + if (memoTextField != null) { + memoField( + memoField = memoTextField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) + } listHeaderItem( titleRes = if (state.isAccountsMode == true) { R.string.common_accounts @@ -153,40 +156,38 @@ private fun LazyListScope.addressItem( } private fun LazyListScope.memoField( - memoField: DestinationTextFieldUM.RecipientMemo?, + memoField: DestinationTextFieldUM.RecipientMemo, onMemoChange: (String, Boolean) -> Unit, ) { - if (memoField != null) { - item(key = MEMO_FIELD_KEY) { - val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText - TextFieldWithPaste( - value = memoField.value, - label = memoField.label, - placeholder = placeholder, - footer = annotatedReference( - buildAnnotatedString { - append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) - append("\n") - withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { - appendColored( - text = stringResourceSafe( - R.string.send_recipient_memo_footer_v2_highlighted, - ), - color = TangemTheme.colors.text.secondary, - ) - } - }, - ), - onValueChange = { onMemoChange(it, false) }, - onPasteClick = { onMemoChange(it, true) }, - modifier = Modifier.padding(top = 20.dp), - labelStyle = TangemTheme.typography.subtitle2, - isError = memoField.isError, - error = memoField.error, - isReadOnly = !memoField.isEnabled, - isValuePasted = memoField.isValuePasted, - ) - } + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = annotatedReference( + buildAnnotatedString { + append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) + append("\n") + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = stringResourceSafe( + R.string.send_recipient_memo_footer_v2_highlighted, + ), + color = TangemTheme.colors.text.secondary, + ) + } + }, + ), + onValueChange = { onMemoChange(it, false) }, + onPasteClick = { onMemoChange(it, true) }, + modifier = Modifier.padding(top = 20.dp), + labelStyle = TangemTheme.typography.subtitle2, + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, + ) } } @@ -297,8 +298,8 @@ private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () (slideInHorizontally() + fadeIn()) .togetherWith(slideOutVertically() + fadeOut()) }, - ) { - if (it) { + ) { currentVisibility -> + if (currentVisibility) { content() } else { Box(modifier = Modifier.fillMaxWidth()) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index 169610ae98..2d1b56c8bb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -60,7 +60,7 @@ internal class BitcoinCustomFeeConverter( amount = feeValue, decimals = value.amount.decimals, txSize = value.txSize, - ).toString(), + )?.toString().orEmpty(), decimals = SATOSHI_DECIMALS, symbol = "", title = resourceReference(R.string.send_satoshi_per_byte_title), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index 1467aada51..65e2574a85 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -39,14 +39,16 @@ internal class EthereumCustomFeeConverter( override fun convert(value: Fee.Ethereum): ImmutableList { return buildList { - convertFeeValue(value).let(::add) + add(convertFeeValue(value)) - when (value) { - is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value) - is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value) - }.let(::addAll) + addAll( + when (value) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value) + is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value) + }, + ) - convertGasLimitValue(value).let(::add) + add(convertGasLimitValue(value)) } .toImmutableList() } @@ -72,8 +74,18 @@ internal class EthereumCustomFeeConverter( value: String, ): ImmutableList { return when (feeValue) { - is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(feeValue, customValues, index, value) - is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange(feeValue, customValues, index, value) + is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange( + feeValue = feeValue, + customValues = customValues, + index = index, + value = value, + ) + is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange( + feeValue = feeValue, + customValues = customValues, + index = index, + value = value, + ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index 66607e3750..28c949d37e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -101,15 +101,16 @@ internal class KaspaCustomFeeConverter( // check that there is reveal transaction info (= krc-20 token transfer) // return without changes otherwise if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) { - getOrNull(FEE_AMOUNT_INDEX)?.let { - val valueDecimal = it.value.parseToBigDecimal(it.decimals) + val customFeeFieldUM = getOrNull(FEE_AMOUNT_INDEX) + if (customFeeFieldUM != null) { + val valueDecimal = customFeeFieldUM.value.parseToBigDecimal(customFeeFieldUM.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) + val fixedValue = minimumFeeAmountValue.parseBigDecimal(customFeeFieldUM.decimals) set( FEE_AMOUNT_INDEX, - it.copy( + customFeeFieldUM.copy( value = fixedValue, label = getFiatReference( rate = currencyStatus.fiatRate, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt index eeaeb2e51d..784e8dd40c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt @@ -7,7 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM internal sealed class NotificationsAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt index e79d598f5e..b44e37a21d 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt @@ -167,7 +167,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test { isPrimaryButtonEnabled = true, walletName = mockk(relaxed = true), isSending = false, - showTapHelp = false, + isShowTapHelp = false, sendingFooter = mockk(relaxed = true), notifications = persistentListOf(), ) diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 803e634a9f..4e3050ec47 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -181,7 +181,7 @@ class SendConfirmationNotificationsTransformerV2Test { isPrimaryButtonEnabled = true, walletName = mockk(relaxed = true), isSending = false, - showTapHelp = false, + isShowTapHelp = false, sendingFooter = mockk(relaxed = true), notifications = persistentListOf(), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 2ed61e7552..611010e6f2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -171,7 +171,7 @@ internal class WcSendTransactionModel @Inject constructor( feeReloadState.value = false modelScope.launch { feeSelectorReloadTrigger.triggerUpdate( - FeeSelectorData(removeSuggestedFee = feeStateConfiguration !is FeeStateConfiguration.Suggestion), + FeeSelectorData(isRemoveSuggestedFee = feeStateConfiguration !is FeeStateConfiguration.Suggestion), ) } } From 54fe752d4a163f7b17fb2c0450b406d3c9756ff7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 11:19:30 +0300 Subject: [PATCH 059/130] Updated on 2026-08-14 --- features/onramp/impl/build.gradle.kts | 2 ++ .../redirect/DefaultOnrampRedirectComponent.kt | 4 ++-- .../redirect/model/OnrampRedirectModel.kt | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 44736bee00..290dd27ce5 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -48,6 +48,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.transaction.models) implementation(projects.domain.account.status) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) /** DI */ implementation(deps.hilt.android) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt index 3f0059348b..cd879af404 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -42,9 +42,9 @@ internal class DefaultOnrampRedirectComponent @AssistedInject constructor( BackHandler(onBack = params.onBack) OnrampRedirectContent(modifier = modifier, state = model.state) - val isDarkTheme = isSystemInDarkTheme() + val isSystemInDarkTheme = isSystemInDarkTheme() LaunchedEffect(model.state) { - model.getRedirectUrl(isDarkTheme = isDarkTheme) + model.getRedirectUrl(isSystemInDarkTheme = isSystemInDarkTheme) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index c803a99746..a550929c72 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onramp.redirect.model +import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -13,6 +14,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError @@ -24,7 +27,9 @@ import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM import com.tangem.features.onramp.success.OnrampSuccessScreenListener import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -38,6 +43,7 @@ internal class OnrampRedirectModel @Inject constructor( private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, private val onrampSuccessScreenListener: OnrampSuccessScreenListener, + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, private val appRouter: AppRouter, paramsContainer: ParamsContainer, getWalletsUseCase: GetWalletsUseCase, @@ -74,8 +80,17 @@ internal class OnrampRedirectModel @Inject constructor( subscribeToOnrampSuccessListener() } - fun getRedirectUrl(isDarkTheme: Boolean) { + fun getRedirectUrl(isSystemInDarkTheme: Boolean) { modelScope.launch { + val isDarkTheme = getAppThemeModeUseCase() + .map { result -> + when (result.getOrElse { AppThemeMode.FOLLOW_SYSTEM }) { + AppThemeMode.FORCE_DARK -> true + AppThemeMode.FORCE_LIGHT -> false + AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme + } + }.firstOrNull() ?: isSystemInDarkTheme + getOnrampRedirectUrlUseCase.invoke( userWallet = selectedUserWallet, quote = params.onrampProviderWithQuote, From 9b134d7058d09dea50730eb2a6e98ced3c222d22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 12:21:04 +0400 Subject: [PATCH 060/130] Updated on 2026-08-14 --- .../configurations/KotlinConfigurations.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index 1ff0e4258f..50de4b0279 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -1,21 +1,29 @@ package com.tangem.plugin.configuration.configurations import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile internal fun Project.configureKotlinCompilerOptions() { + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + kotlinExtension.sourceSets.all { // https://github.com/Kotlin/KEEP/blob/explicit-backing-fields-re/proposals/explicit-backing-fields.md languageSettings.enableLanguageFeature("ExplicitBackingFields") } project.tasks.withType { - kotlinOptions { - jvmTarget = "17" - allWarningsAsErrors = false + compilerOptions { + allWarningsAsErrors.set(false) // this is required to produce a unique META-INF/*.kotlin_module files - moduleName = project.path.removePrefix(":").replace(':', '-') + freeCompilerArgs.add("-module-name=${project.path.removePrefix(":").replace(':', '-')}") } } } \ No newline at end of file From bc76eaa64a470718326c5820d910542d39801fb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 10:01:22 +0100 Subject: [PATCH 061/130] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tangem/tap/di/domain/NewsDomainModule.kt | 11 +- .../com/tangem/common/ui/news/ArticleCard.kt | 11 +- .../common/ui/news/ArticleLoadingCard.kt | 63 +++++++++ .../com/tangem/datasource/api/news/NewsApi.kt | 2 +- .../models/response/NewsTrendingResponse.kt | 6 - .../bottomsheets/state}/BottomSheetState.kt | 2 +- .../tangem/core/ui/components/label/Label.kt | 31 ++-- .../ui/components/label/entity/LabelUM.kt | 2 +- .../core/ui/utils/DateTimeFormatters.kt | 11 ++ .../news/repository/DefaultNewsRepository.kt | 7 +- .../domain/news/repository/NewsRepository.kt | 4 +- .../news/usecase/FetchTrendingNewsUseCase.kt | 23 +++ .../entry/components/FeedEntryComponent.kt | 2 +- features/feed/impl/build.gradle.kts | 2 + .../components/DefaultFeedEntryComponent.kt | 20 ++- .../feed/components/FeedEntryChildFactory.kt | 6 +- .../components/feed/DefaultFeedComponent.kt | 18 ++- .../features/feed/di/ComponentModule.kt | 18 +++ .../tangem/features/feed/di/ModelModule.kt | 20 +++ .../feed/model/feed/FeedComponentModel.kt | 103 ++++++++++++++ .../feed/ui/EntryBottomSheetContent.kt | 48 +++++++ .../tangem/features/feed/ui/feed/FeedList.kt | 132 ++++++++++-------- .../features/feed/ui/feed/FeedListLoading.kt | 100 +++++++++++++ .../preview/FeedListPreviewDataProvider.kt | 9 +- .../features/feed/ui/feed/state/FeedListUM.kt | 10 +- .../ui/feed/state/SearchBarStateFactory.kt | 24 ++++ .../ui/feed/state/TrendingNewsStateFactory.kt | 83 +++++++++++ .../details/MarketsTokenDetailsComponent.kt | 2 +- .../markets/entry/BottomSheetState.kt | 6 - .../markets/entry/MarketsEntryComponent.kt | 1 + .../tokenlist/MarketsTokenListComponent.kt | 2 +- .../DefaultMarketsTokenDetailsComponent.kt | 2 +- .../impl/DefaultMarketsEntryComponent.kt | 2 +- .../entry/impl/ui/EntryBottomSheetContent.kt | 2 +- .../impl/DefaultMarketsTokenListComponent.kt | 2 +- .../tokenlist/impl/model/MarketsListModel.kt | 2 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/WalletComponent.kt | 54 ++++++- .../wallet/child/wallet/model/WalletModel.kt | 9 ++ .../common/preview/WalletScreenPreviewData.kt | 1 + .../wallet/state/WalletStateController.kt | 1 + .../wallet/state/model/WalletScreenState.kt | 1 + .../presentation/wallet/ui/WalletScreen.kt | 55 ++++---- .../extension/BaseExtensionConfigurations.kt | 4 +- 46 files changed, 764 insertions(+), 154 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt rename {features/feed/api/src/main/kotlin/com/tangem/features/feed/entry => core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state}/BottomSheetState.kt (51%) create mode 100644 domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5b03c3345..bb2b8ab488 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -215,6 +215,7 @@ dependencies { implementation(projects.data.walletManager) implementation(projects.data.yieldSupply) implementation(projects.data.hotWallet) + implementation(projects.data.news) /** Features */ implementation(projects.features.referral.impl) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt index dda3134a35..55b1457a55 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.news.repository.NewsRepository -import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase -import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase -import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase -import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.domain.news.usecase.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -38,4 +35,10 @@ internal object NewsDomainModule { fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase { return GetNewsListBatchFlowUseCase(repository) } + + @Provides + @Singleton + fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase { + return FetchTrendingNewsUseCase(repository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index a1ee6a610c..70d50e3b81 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardColors import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -17,6 +18,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference @@ -30,10 +32,16 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @Composable -fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { BlockCard( modifier = modifier, onClick = onArticleClick, + colors = colors, ) { if (articleConfigUM.isTrending) { TrendingArticle(articleConfigUM = articleConfigUM) @@ -167,7 +175,6 @@ private fun ArticleCardsPreview() { LabelUM(TextReference.Str("BTC")), LabelUM(TextReference.Str("Supply")), LabelUM(TextReference.Str("Demand")), - LabelUM(TextReference.Str("Best rate")), LabelUM(TextReference.Str("Breaking news")), ).toImmutableSet() diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt new file mode 100644 index 0000000000..4ef5f1ce75 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt @@ -0,0 +1,63 @@ +package com.tangem.common.ui.news + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun TrendingLoadingArticle(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp) + SpacerH(12.dp) + RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp) + SpacerH(14.dp) + RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp) + SpacerH(32.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + } + } + } +} + +@Composable +fun DefaultLoadingArticle() { + BlockCard( + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column(modifier = Modifier.padding(12.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp) + SpacerH(12.dp) + RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp) + SpacerH(16.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt index 24e359b20e..d0e5ab9707 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt @@ -38,6 +38,6 @@ interface NewsApi { private companion object { - private const val NEWS_PATH = "api/v1/news" + private const val NEWS_PATH = "v1/news" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt index 3a21af3d04..91464cb405 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt @@ -5,11 +5,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class NewsTrendingResponse( - @Json(name = "meta") val meta: NewsTrendingMetaDto, @Json(name = "items") val items: List, -) - -@JsonClass(generateAdapter = true) -data class NewsTrendingMetaDto( - @Json(name = "limit") val limit: Int, ) \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt similarity index 51% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt index 9b58eb7690..70d201ddd5 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.entry +package com.tangem.core.ui.components.bottomsheets.state enum class BottomSheetState { EXPANDED, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 36e1eae057..0f6b07e0d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -17,13 +18,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelStyle @@ -108,11 +111,23 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { state.leadingContent.let { leadingContentUM -> when (leadingContentUM) { is LabelLeadingContentUM.Token -> { - Icon( - modifier = Modifier - .size(16.dp), - painter = painterResource(leadingContentUM.icon), - tint = Color.Unspecified, + SubcomposeAsyncImage( + modifier = Modifier.size(16.dp), + model = ImageRequest.Builder(context = LocalContext.current) + .data(leadingContentUM.iconUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, contentDescription = null, ) } @@ -168,7 +183,7 @@ private fun LabelPreview() { Label( state = LabelUM( leadingContent = LabelLeadingContentUM.Token( - icon = R.drawable.img_bsc_22, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", ), text = TextReference.Str("Regular Label"), style = LabelStyle.REGULAR, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt index afa0e60bae..131469bd96 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -17,7 +17,7 @@ data class LabelUM( @Immutable sealed class LabelLeadingContentUM { data object None : LabelLeadingContentUM() - data class Token(val icon: Int) : LabelLeadingContentUM() + data class Token(val iconUrl: String) : LabelLeadingContentUM() } enum class LabelStyle { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 85d0294996..8a5cae1ae4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -1,6 +1,10 @@ package com.tangem.core.ui.utils import android.text.format.DateFormat +import com.tangem.core.ui.utils.DateTimeFormatters.dateDDMMYYYY +import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd +import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter +import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -80,6 +84,13 @@ object DateTimeFormatters { getBestFormatterBySkeleton("yyyy") } + /** + * Example: "June 31" + */ + val dateDMMM: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("d MMMM") + } + /** * Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM" */ diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index e42068534e..426bc6f287 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -18,7 +18,6 @@ import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -31,7 +30,7 @@ import kotlin.collections.orEmpty * Implementation of [NewsRepository]. [REDACTED_AUTHOR] */ -internal class DefaultNewsRepository @Inject constructor( +internal class DefaultNewsRepository( private val newsApi: NewsApi, private val dispatchers: CoroutineDispatcherProvider, private val newsDetailsStore: NewsDetailsStore, @@ -68,8 +67,8 @@ internal class DefaultNewsRepository @Inject constructor( fetchDetailedArticlesInternal(newsIds = newsIds, language = language) } - override suspend fun getTrendingNews(limit: Int, language: String?): List { - return fetchAndStoreTrendingNews(limit = limit, language = language) + override suspend fun getTrendingNews(limit: Int, language: String?) { + fetchAndStoreTrendingNews(limit = limit, language = language) } override fun observeTrendingNews(): Flow> { diff --git a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt index c8ab6c6cd6..7db6669b0d 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt @@ -38,12 +38,12 @@ interface NewsRepository { suspend fun fetchDetailedArticles(newsIds: Collection, language: String?) /** - * Returns list of trending news by limit and with correct locale. + * Fetch list of trending news by limit and with correct locale and store it in runtime data store. * * @param limit * @param language current device locale */ - suspend fun getTrendingNews(limit: Int, language: String?): List + suspend fun getTrendingNews(limit: Int, language: String?) /** * Observes trending news with runtime viewed flag support. diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt new file mode 100644 index 0000000000..319f111625 --- /dev/null +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.news.usecase + +import arrow.core.Either +import com.tangem.domain.news.repository.NewsRepository +import java.util.Locale + +/** + * Fetches trending news to store it in runtime data store. + */ + +class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) { + + suspend operator fun invoke(): Either = Either.catch { + newsRepository.getTrendingNews( + limit = LIMIT_FOR_TRENDING_NEWS, + language = Locale.getDefault().language, + ) + } + + companion object { + private const val LIMIT_FOR_TRENDING_NEWS = 10 + } +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt index ad7aab9506..43d68d3d11 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.features.feed.entry.BottomSheetState +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface FeedEntryComponent { diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index c309d45ccf..cadd191a0e 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.transaction) + implementation(projects.domain.news) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. @@ -86,6 +87,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index cdf3a46598..d27de2ff81 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -1,10 +1,13 @@ package com.tangem.features.feed.components +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -13,11 +16,13 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent -import com.tangem.features.feed.entry.BottomSheetState import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.ui.EntryBottomSheetContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) - private val stack: Value> = childStack( + private val stack: Value> = childStack( key = "main", source = stackNavigation, serializer = FeedEntryChildFactory.Child.serializer(), @@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { - bottomSheetState // TODO will be continued in next tasks. + val stackState by stack.subscribeAsState() + + BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) { + onChildBack() + } + + EntryBottomSheetContent( + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, + ) } private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index c33781008b..6483e57566 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.components import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.feed.DefaultFeedComponent @@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent import com.tangem.features.feed.components.news.list.DefaultNewsListComponent import kotlinx.serialization.Serializable +import javax.inject.Inject -internal class FeedEntryChildFactory { +internal class FeedEntryChildFactory @Inject constructor() { @Serializable @Immutable @@ -43,7 +45,7 @@ internal class FeedEntryChildFactory { child: Child, appComponentContext: AppComponentContext, onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { + ): ComposableModularContentComponent { return when (child) { is Child.TokenDetails -> { DefaultMarketsTokenDetailsComponent( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index a6bd119f49..910542c05a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -1,23 +1,37 @@ package com.tangem.features.feed.components.feed import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.model.feed.FeedComponentModel +import com.tangem.features.feed.ui.feed.FeedListContent +import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + private val feedComponentModel = getOrCreateModel() + @Composable override fun Title() { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListHeader(state.searchBar) } @Composable override fun Content(modifier: Modifier) { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListContent( + modifier = modifier, + state = state, + ) } @Composable - override fun Footer() { - } + override fun Footer() = Unit } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt new file mode 100644 index 0000000000..5c06261458 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.di + +import com.tangem.features.feed.components.DefaultFeedEntryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt new file mode 100644 index 0000000000..f00b75a4b1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.feed.model.feed.FeedComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(FeedComponentModel::class) + fun bindsFeedComponentModel(model: FeedComponentModel): Model +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt new file mode 100644 index 0000000000..5445a03eed --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -0,0 +1,103 @@ +package com.tangem.features.feed.model.feed + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase +import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.state.* +import com.tangem.features.feed.ui.market.state.SortByTypeUM +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentHashMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import javax.inject.Inject + +@Stable +@ModelScoped +internal class FeedComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, + private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, +) : Model() { + + private val _state = MutableStateFlow(initialState()) + val state = _state.asStateFlow() + + private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) { + SearchBarStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) { + TrendingNewsStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + init { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + subscribeOnTrendingNews() + } + _state.update { feedListUM -> + feedListUM.copy( + searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + ) + } + } + + private suspend fun subscribeOnTrendingNews() { + manageTrendingNewsUseCase().collect { articles -> + trendingNewsStateFactory.updateTrendingNewsState(articles) + } + } + + private fun initialState(): FeedListUM { + return FeedListUM( + currentDate = getCurrentDate(), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.markets_search_header_title), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + feedListCallbacks = FeedListCallbacks( + onSearchClick = {}, + onMarketOpenClick = {}, + onArticleClick = {}, + onOpenAllNews = {}, + onMarketItemClick = {}, + onSortTypeClick = {}, + ), + news = NewsUM.Loading, + trendingArticle = null, + marketChartConfig = MarketChartConfig( + marketCharts = buildMap { + SortByTypeUM.entries.forEach { + put(it, MarketChartUM.Loading) + } // TODO in [REDACTED_TASK_KEY] add correct sorting + }.toPersistentHashMap(), + currentSortByType = SortByTypeUM.TopGainers, + ), + ) + } + + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt new file mode 100644 index 0000000000..cf87b0cefd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.feed.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.components.FeedEntryChildFactory + +@Composable +internal fun EntryBottomSheetContent( + stackState: ChildStack, + onHeaderSizeChange: (Dp) -> Unit, +) { + val density = LocalDensity.current + + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + topBar = { + AnimatedContent( + targetState = stackState.active.instance, + modifier = Modifier.onGloballyPositioned { coordinates -> + if (coordinates.size.height > 0) { + with(density) { + onHeaderSizeChange(coordinates.size.height.toDp()) + } + } + }, + ) { currentState -> + currentState.Title() + } + }, + content = { contentPadding -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Content(modifier = Modifier.padding(contentPadding)) + } + }, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 51c97516b7..82cde520e1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -22,25 +22,25 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.news.ArticleCard +import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -48,51 +48,44 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState -import com.tangem.features.feed.ui.feed.state.FeedListCallbacks -import com.tangem.features.feed.ui.feed.state.FeedListUM -import com.tangem.features.feed.ui.feed.state.MarketChartConfig -import com.tangem.features.feed.ui.feed.state.MarketChartUM +import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.components.MarketsListItem import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM -import kotlinx.collections.immutable.ImmutableList @Composable -internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { - val density = LocalDensity.current +internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + SearchBar( + modifier = modifier + .drawBehind { drawRect(background) } + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + state = searchBarUM, + colors = TangemSearchBarDefaults.defaultTextFieldColors.copy( + focusedContainerColor = TangemTheme.colors.field.focused, + unfocusedContainerColor = TangemTheme.colors.field.focused, + ), + ) +} + +@Composable +internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .drawBehind { drawRect(background) }, ) { - SearchBar( - modifier = Modifier - .drawBehind { drawRect(background) } - .padding( - start = 16.dp, - end = 16.dp, - bottom = 8.dp, - ) - .onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - } - .padding(bottom = 4.dp), - state = state.searchBar, - ) - SpacerH(20.dp) Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = stringResourceSafe(R.string.feed_market_and_news), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = state.currentDate, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.tertiary, @@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: @Composable private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { if (marketChartConfig.marketCharts.isNotEmpty()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, + ) + LazyRow( modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -175,17 +179,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb } } - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, - ) - SpacerH(12.dp) AnimatedContent( @@ -207,18 +200,38 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb @Suppress("CanBeNonNullable") @Composable -private fun NewsBlock( +private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { + AnimatedContent(news) { newsUM -> + when (newsUM) { + is NewsUM.Content -> { + if (newsUM.content.isNotEmpty()) { + NewsContentBlock( + feedListCallbacks = feedListCallbacks, + news = newsUM, + trendingArticle = trendingArticle, + ) + } + } + NewsUM.Loading -> { + NewsLoadingBlock() + } + } + } +} + +@Composable +private fun NewsContentBlock( feedListCallbacks: FeedListCallbacks, - news: ImmutableList, + news: NewsUM.Content, trendingArticle: ArticleConfigUM?, ) { - if (news.isNotEmpty()) { + Column { Header( title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) @@ -244,13 +257,12 @@ private fun NewsBlock( append(stringResourceSafe(R.string.feed_tangem_ai)) } }, - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, ) } }, onSeeAllClick = feedListCallbacks.onOpenAllNews, ) - SpacerH(12.dp) trendingArticle?.let { article -> @@ -260,8 +272,8 @@ private fun NewsBlock( .padding(horizontal = 16.dp), articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) - SpacerH(12.dp) } @@ -272,13 +284,16 @@ private fun NewsBlock( state = rememberLazyListState(), ) { items( - items = news, + items = news.content, key = ArticleConfigUM::id, ) { article -> ArticleCard( articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = Modifier.size(164.dp), + modifier = Modifier + .height(164.dp) + .widthIn(max = 216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) } } @@ -292,6 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) { .fillMaxWidth() .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { title() @@ -310,7 +326,10 @@ private fun Charts( onItemClick: (MarketsListItemUM) -> Unit, modifier: Modifier = Modifier, ) { - BlockCard(modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { Column( modifier = Modifier .fillMaxWidth() @@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED) @Composable private fun FeedListPreview() { TangemThemePreview { - FeedList( - state = createFeedPreviewState(), - onHeaderSizeChange = {}, - ) + FeedListContent(state = createFeedPreviewState()) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt new file mode 100644 index 0000000000..69f94e61e6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -0,0 +1,100 @@ +package com.tangem.features.feed.ui.feed + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.DefaultLoadingArticle +import com.tangem.common.ui.news.TrendingLoadingArticle +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder + +@Composable +internal fun MarketLoadingBlock() { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun MarketPulseLoadingBlock() { + RectangleShimmer() + SpacerH(8.dp) + LazyRow( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp)) + } + } + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun NewsLoadingBlock() { + Column { + RectangleShimmer() + SpacerH(12.dp) + TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(12.dp) + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + DefaultLoadingArticle() + } + } + } +} + +@Composable +private fun ChartsLoading(modifier: Modifier = Modifier) { + BlockCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + repeat(DEFAULT_CHART_SIZE_IN_MARKET) { + MarketsListItemPlaceholder() + } + } + } +} + +private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 + +@Preview(showBackground = true) +@Composable +private fun FeedListLoadingPreview() { + TangemThemePreview { + Column { + NewsLoadingBlock() + SpacerH(10.dp) + MarketLoadingBlock() + SpacerH(10.dp) + MarketPulseLoadingBlock() + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 9eaf7e5c4b..747d3d49bc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -1,16 +1,15 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -39,7 +38,7 @@ internal object FeedListPreviewDataProvider { onMarketItemClick = {}, onSortTypeClick = {}, ), - news = articles.filter { it.isTrending.not() }.toImmutableList(), + news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()), trendingArticle = articles.first { it.isTrending }, marketChartConfig = MarketChartConfig( marketCharts = createMarketCharts(marketItems, includeErrorState = false), @@ -165,7 +164,7 @@ internal object FeedListPreviewDataProvider { LabelUM( text = TextReference.Str("BTC"), leadingContent = LabelLeadingContentUM.Token( - icon = R.drawable.img_btc_22, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", ), ), LabelUM(TextReference.Str("Regulation")), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 7e002be1f4..60f842ea9b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -14,7 +14,7 @@ internal data class FeedListUM( val currentDate: String, val searchBar: SearchBarUM, val feedListCallbacks: FeedListCallbacks, - val news: ImmutableList, + val news: NewsUM, val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, ) @@ -28,6 +28,12 @@ internal data class FeedListCallbacks( val onSortTypeClick: (SortByTypeUM) -> Unit, ) +@Immutable +internal sealed interface NewsUM { + data object Loading : NewsUM + data class Content(val content: ImmutableList) : NewsUM +} + internal data class MarketChartConfig( val marketCharts: ImmutableMap, val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers, @@ -49,7 +55,7 @@ internal sealed interface MarketChartUM { data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM } -data class SortChartConfigUM( +internal data class SortChartConfigUM( val sortByType: SortByTypeUM, val isSelected: Boolean, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt new file mode 100644 index 0000000000..bd33df97cc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt @@ -0,0 +1,24 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.utils.Provider + +internal class SearchBarStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + val searchQuery: String + get() = currentStateProvider().searchBar.query + + fun onSearchQueryChange(query: String) { + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + searchBar = currentState.searchBar.copy( + query = query, + isActive = query.isNotEmpty(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt new file mode 100644 index 0000000000..52bc5e2c2f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.news.ShortArticle +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet + +internal class TrendingNewsStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + fun updateTrendingNewsState(news: List) { + val trendingArticleIndex = news.indexOfFirst { it.isTrending } + val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null + val commonArticles = if (trendingArticleIndex != -1) { + news.toMutableList().apply { removeAt(trendingArticleIndex) } + } else { + news + } + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + trendingArticle = trendingArticle?.let { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = true, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }, + news = NewsUM.Content( + commonArticles.map { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = false, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }.toPersistentList(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index 58e368cc07..0136e2532f 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState import kotlinx.serialization.Serializable @Stable diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt deleted file mode 100644 index 07aee6c5d9..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.markets.entry - -enum class BottomSheetState { - EXPANDED, - COLLAPSED, -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt index a30498b380..05a7e68670 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface MarketsEntryComponent { diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt index 5a5179c1ef..8062a9a1f6 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt @@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState @Stable interface MarketsTokenListComponent : ComposableContentComponent { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index b1f66a3c20..54aa223690 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -21,7 +22,6 @@ import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalytics import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 06f3b26d19..9f441298d5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index 9c72accb3a..a19e160096 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory import com.tangem.features.markets.tokenlist.MarketsTokenListComponent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 8e6955c9b0..14e1c4dc72 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -16,12 +16,12 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.toSerializableParam -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import com.tangem.features.markets.tokenlist.impl.ui.MarketsList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 22574f445e..600133bc7c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -6,6 +6,7 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase @@ -17,7 +18,6 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 041df48f96..5b80958c46 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -46,7 +47,6 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index ca7ea45cc6..81e2a852c5 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) implementation(projects.features.tangempay.details.api) + implementation(projects.features.feed.api) /** Common modules */ implementation(projects.common) 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 da07a45435..32454e1a3f 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 @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.child.wallet -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent @@ -23,6 +25,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams @@ -37,16 +41,25 @@ import kotlinx.coroutines.launch internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, + marketsEntryComponentFactory: MarketsEntryComponent.Factory, + feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, - private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, + private val feedFeatureToggle: FeedFeatureToggle, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() + private val feedEntryComponent by lazy { + feedEntryComponentFactory.create(child("feedEntryComponent")) + } + private val marketsEntryComponent by lazy { + marketsEntryComponentFactory.create(child("marketsEntryComponent")) + } + init { lifecycle.subscribe(model.screenLifecycleProvider) doOnResume { model.onResume() } @@ -114,15 +127,23 @@ internal class WalletComponent @AssistedInject constructor( }, ) - private val marketsEntryComponent = marketsEntryComponentFactory.create(child("marketsEntryComponent")) - @Composable override fun Content(modifier: Modifier) { + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } + var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() WalletScreen( state = model.uiState.collectAsStateWithLifecycle().value, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, ) when (val dialog = dialog.child?.instance) { @@ -132,6 +153,27 @@ internal class WalletComponent @AssistedInject constructor( } } + @Composable + private fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier = Modifier, + ) { + if (feedFeatureToggle.isFeedEnabled) { + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } else { + marketsEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + } + @AssistedFactory interface Factory { fun create(appComponentContext: AppComponentContext, navigate: (WalletRoute) -> Unit): WalletComponent 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 586ccfc586..960d3c37f8 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 @@ -42,6 +42,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.tangempay.TangemPayFeatureToggles @@ -99,6 +100,7 @@ internal class WalletModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val trackingContextProxy: TrackingContextProxy, + private val feedFeatureToggle: FeedFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -121,6 +123,7 @@ internal class WalletModel @Inject constructor( analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy()) } + updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -143,6 +146,12 @@ internal class WalletModel @Inject constructor( } } + private fun updateMarketToggle() { + stateHolder.update { + it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) + } + } + private fun updateYieldSupplyApy() { if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { modelScope.launch(dispatchers.default) { 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 c93c2cfd4d..e617d2d783 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 @@ -223,6 +223,7 @@ internal object WalletScreenPreviewData { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) internal val accountScreenState = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index de8c2ee08a..572e187d6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -110,6 +110,7 @@ internal class WalletStateController @Inject constructor() { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index df260b5856..1fb6a6d24c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -13,5 +13,6 @@ internal data class WalletScreenState( val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, + val isNewMarketEnabled: Boolean, val onDismissMarketsOnboarding: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 16212dc865..1f2e8441c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -51,6 +51,7 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* @@ -87,15 +88,18 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPa import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.entry.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) { +internal fun WalletScreen( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { // It means that screen is still initializing if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return @@ -116,8 +120,10 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = bottomSheetContent, alertConfig = alertConfig, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, ) WalletEventEffect( @@ -136,9 +142,11 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsEntryComponent: MarketsEntryComponent, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), ) { /* * Don't pass key to remember, because it will brake scroll animation. @@ -286,25 +294,15 @@ private fun WalletContent( ) } - val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } - - var headerSize by remember { mutableStateOf(0.dp) } - BaseScaffoldWithMarkets( state = state, listState = listState, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = { headerSize }, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, alertConfig = alertConfig, - onBottomSheetStateChange = { bottomSheetState.value = it }, - bottomSheetContent = { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = Modifier, - ) - }, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, content = scaffoldContent, ) } @@ -394,7 +392,11 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val backgroundPrimary = TangemTheme.colors.background.primary + val background = if (state.isNewMarketEnabled) { + TangemTheme.colors.background.tertiary + } else { + TangemTheme.colors.background.primary + } val showMarketsHint by remember { derivedStateOf { @@ -407,7 +409,7 @@ private inline fun BaseScaffoldWithMarkets( } CompositionLocalProvider( - LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) }, + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { val backgroundColor = LocalMainBottomSheetColor.current var isSearchFieldFocused by remember { mutableStateOf(false) } @@ -798,16 +800,11 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsEntryComponent = object : MarketsEntryComponent { - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - Text("Markets Content") - } + bottomSheetContent = { + Text("Markets Content") }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, ) } } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 7011f2d423..bb9c96653e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -3,11 +3,8 @@ package com.tangem.plugin.configuration.configurations.extension import com.android.build.gradle.BaseExtension import com.tangem.plugin.configuration.model.AppConfig import com.tangem.plugin.configuration.utils.findPlugin -import com.tangem.plugin.configuration.utils.findVersion import org.gradle.api.JavaVersion import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.plugins internal fun BaseExtension.configureCompileSdk() { compileSdkVersion(AppConfig.compileSdkVersion) @@ -28,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:feed:api\$")) || // provides Composable function contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) From 5f938d613922661aa5d7c0fdb4b736b7458638e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 14:15:13 +0500 Subject: [PATCH 062/130] Updated on 2026-08-14 --- .../models/event/OnboardingAnalyticsEvent.kt | 17 ++++++++++++----- .../CreateWalletSelectionModel.kt | 3 +++ .../createwalletstart/CreateWalletStartModel.kt | 3 +++ .../port/model/AddExistingWalletImportModel.kt | 4 ++-- .../CreateMobileWalletModel.kt | 12 ++++++++---- .../v2/common/analytics/OnboardingEvent.kt | 3 +++ .../model/MultiWalletCreateWalletModel.kt | 8 +++++++- .../model/MultiWalletSeedPhraseModel.kt | 11 +++++++++++ .../model/OnboardingNoteCreateWalletModel.kt | 15 +++++++++++---- .../v2/note/impl/model/OnboardingNoteModel.kt | 5 +++++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 6 +++++- 11 files changed, 70 insertions(+), 17 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt index d10949860f..f4bc6d2ca5 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt @@ -47,7 +47,7 @@ sealed class OnboardingAnalyticsEvent( params: Map = mapOf(), ) : OnboardingAnalyticsEvent(category = "Onboarding / Create Wallet", event = event, params = params) { - data object ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class ButtonCreateWallet : CreateWallet("Button - Create Wallet") class WalletCreatedSuccessfully( source: String, @@ -77,10 +77,17 @@ sealed class OnboardingAnalyticsEvent( params: Map = mapOf(), ) : OnboardingAnalyticsEvent(category = "Onboarding / Seed Phrase", event = event, params = params) { - data object CreateMobileScreenOpened : SeedPhrase("Create Mobile Screen Opened") - data object ButtonImportWallet : SeedPhrase("Button - Import Wallet") - data object ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened") - data object ButtonImport : SeedPhrase("Button - Import") + class CreateMobileScreenOpened( + source: String, + ) : SeedPhrase( + event = "Create Mobile Screen Opened", + params = mapOf( + AnalyticsParam.SOURCE to source, + ), + ) + class ButtonImportWallet : SeedPhrase("Button - Import Wallet") + class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened") + class ButtonImport : SeedPhrase("Button - Import") } sealed class Error( 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 48a41b6f98..2ee891e036 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 @@ -5,6 +5,7 @@ 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.OnboardingAnalyticsEvent +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 @@ -32,6 +33,7 @@ import javax.inject.Inject internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, @@ -99,6 +101,7 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onMobileWalletClick() { + trackingContextProxy.addHotWalletContext() analyticsEventHandler.send( event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( source = AnalyticsParam.ScreensSources.AddNewWallet.value, 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 554c0becb1..ef8ef65b13 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 @@ -8,6 +8,7 @@ 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.OnboardingAnalyticsEvent +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -60,6 +61,7 @@ internal class CreateWalletStartModel @Inject constructor( @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -141,6 +143,7 @@ internal class CreateWalletStartModel @Inject constructor( } private fun onStartWithMobileWalletClick() { + trackingContextProxy.addHotWalletContext() analyticsEventHandler.send( event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( source = AnalyticsParam.ScreensSources.CreateWalletIntro.value, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index d891de2892..4245e9f79d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -64,7 +64,7 @@ internal class AddExistingWalletImportModel @Inject constructor( } init { - analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened) + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened()) importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, mnemonicRepository = mnemonicRepository, @@ -77,7 +77,7 @@ internal class AddExistingWalletImportModel @Inject constructor( ) }, onPassphraseInfoClick = ::onPassphraseInfoClick, - onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport) }, + onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) }, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 243343ce0b..0f188dfe43 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -60,8 +60,12 @@ internal class CreateMobileWalletModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() - analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Started(source = params.source)) - analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source), + ) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened(source = params.source), + ) } override fun onDestroy() { @@ -70,13 +74,13 @@ internal class CreateMobileWalletModel @Inject constructor( } private fun onImportClick() { - analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet) + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet()) checkHotWalletCreationSupported(notSupported = { return }) router.push(AppRoute.AddExistingWallet) } private fun onCreateClick() { - analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet) + analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet()) checkHotWalletCreationSupported(notSupported = { return }) modelScope.launch { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 2050039cc6..1aa8695bab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -19,13 +19,16 @@ sealed class OnboardingEvent( class ScreenOpened : CreateWallet("Create Wallet Screen Opened") class ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class ButtonOtherOptions : CreateWallet("Button - Other Options") class WalletCreatedSuccessfully( creationType: WalletCreationType = WalletCreationType.PrivateKey, seedPhraseLength: Int? = null, + passPhraseState: AnalyticsParam.EmptyFull, ) : CreateWallet( event = "Wallet Created Successfully", params = buildMap { put("Creation Type", creationType.value) + put("Passphrase", passPhraseState.value) if (seedPhraseLength != null) { put("Seed Phrase Length", seedPhraseLength.toString()) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 45bb77cd1c..1cb37f4eb6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -68,6 +69,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( }, showOtherOptionsButton = params.parentParams.withSeedPhraseFlow, onOtherOptionsClick = { + analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonOtherOptions()) modelScope.launch { onDone.emit(Step.SeedPhrase) } @@ -104,7 +106,11 @@ internal class MultiWalletCreateWalletModel @Inject constructor( cardRepository.startCardActivation(cardId = result.data.card.cardId) - analyticsHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) val cardDoesNotSupportBackup = result.data.card.settings.isBackupAllowed.not() when { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 52538e1e92..0f18033dd3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -5,6 +5,8 @@ import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -60,6 +62,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private val isWalletAlreadySavedUseCase: IsWalletAlreadySavedUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -108,6 +111,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( updateUiState = { block -> updateUiStateSpecific(block) }, readyToImport = { ready -> state.update { it.copy(readyToImport = ready) } }, importWallet = { mnemonic, passphrase -> + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) importWallet( mnemonic = mnemonic, passphrase = passphrase, @@ -149,6 +153,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private fun getInitialUIState(): MultiWalletSeedPhraseUM { return MultiWalletSeedPhraseUM.Start( onImportSeedPhraseClicked = { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet()) openImportSeedPhrase() }, onGenerateSeedPhraseClicked = { @@ -177,6 +182,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( } private fun openImportSeedPhrase() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened()) _uiState.value = importSeedPhraseUiStateBuilder.getState() } @@ -232,6 +238,11 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( OnboardingEvent.CreateWallet.WalletCreationType.SeedImport }, seedPhraseLength = mnemonic.mnemonicComponents.size, + passPhraseState = if (passphrase.isNullOrBlank()) { + AnalyticsParam.EmptyFull.Empty + } else { + AnalyticsParam.EmptyFull.Full + }, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 50850543c5..51db083121 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -1,7 +1,8 @@ package com.tangem.features.onboarding.v2.note.impl.child.create.model import com.tangem.common.CompletionResult -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteCreateWalletModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +32,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( private val cardRepository: CardRepository, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -41,11 +44,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( ) init { - Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened()) + analyticsEventHandler.send(OnboardingEvent.CreateWallet.ScreenOpened()) modelScope.launch { val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) { - Analytics.send(OnboardingEvent.Started()) + analyticsEventHandler.send(OnboardingEvent.Started()) } } observeArtwork() @@ -62,7 +65,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( val result = tangemSdkManager.createProductWallet(scanResponse) when (result) { is CompletionResult.Success -> { - Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsEventHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) } is CompletionResult.Failure -> _uiState.update { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index ddfd182953..04edacf8ef 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.note.impl.model import com.arkivanov.decompose.router.stack.StackNavigation import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,6 +10,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState @@ -22,6 +24,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +33,7 @@ internal class OnboardingNoteModel @Inject constructor( private val messageSender: UiMessageSender, private val getCardImageUseCase: GetCardImageUseCase, private val artworkUMConverter: ArtworkUMConverter, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @Suppress("UnusedPrivateMember") @@ -72,6 +76,7 @@ internal class OnboardingNoteModel @Inject constructor( } fun onWalletCreated(userWallet: UserWallet) { + analyticsEventHandler.send(OnboardingEvent.Finished()) commonUiState.update { it.copy(userWallet = userWallet) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index efa215b517..e70843bd7b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -171,7 +171,11 @@ internal class OnboardingTwinModel @Inject constructor( } } - analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsEventHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) update { it.copy( From eb73a64054785999be9a16dbf84bc0bce3dd95f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 15:11:21 +0500 Subject: [PATCH 063/130] Updated on 2026-08-14 --- .../feature/wallet/child/wallet/model/WalletModel.kt | 6 +----- .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 10 ++++++++-- .../analytics/utils/WalletWarningsAnalyticsSender.kt | 10 +++++++++- 3 files changed, 18 insertions(+), 8 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 960d3c37f8..38cf4e0ca2 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,7 +4,6 @@ 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 @@ -116,7 +115,6 @@ internal class WalletModel @Inject constructor( private val updateTangemPayJobHolder = JobHolder() private var expressTxStatusTaskScheduler = SingleTaskScheduler() - private val hasMainScreenOpenedEventSent = AtomicBoolean(false) init { if (!hotWalletFeatureToggles.isHotWalletEnabled) { @@ -291,15 +289,13 @@ internal class WalletModel @Inject constructor( .onEach { selectedWallet -> trackingContextProxy.setContext(selectedWallet) - if (hotWalletFeatureToggles.isHotWalletEnabled && !hasMainScreenOpenedEventSent.get()) { - // send it here because we need context to be set + if (hotWalletFeatureToggles.isHotWalletEnabled) { modelScope.launch { val hasMobileWallet = userWalletsListRepository.userWalletsSync() .any { it is UserWallet.Hot } analyticsEventsHandler.send( WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(hasMobileWallet), ) - hasMainScreenOpenedEventSent.set(true) } } 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 bdd61799ba..5d679b0b59 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 @@ -62,9 +62,15 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Mobile Wallet" to if (hasMobileWallet) "Yes" else "No"), ) - data class NoticeFinishActivation(private val activationState: ActivationState) : MainScreen( + data class NoticeFinishActivation( + private val activationState: ActivationState, + private val balanceState: AnalyticsParam.EmptyFull, + ) : MainScreen( event = "Notice - Finish Activation", - params = mapOf("Activation State" to activationState.value), + params = mapOf( + "Activation State" to activationState.value, + "Balance State" to balanceState.value, + ), ) { enum class ActivationState(val value: String) { NotStarted("Not Started"), 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 66ad6e1bd0..8cfc8b2ea0 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 @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -78,7 +79,14 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( } else { MainScreen.NoticeFinishActivation.ActivationState.NotStarted } - MainScreen.NoticeFinishActivation(activationState) + val balanceState = when (warning.type) { + WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty + WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full + } + MainScreen.NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) } is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport() is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond() From 00d55ff5a64e8d66915fec41c7422fc243e7b9c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 15:11:58 +0500 Subject: [PATCH 064/130] Updated on 2026-08-14 --- core/ui/detekt-baseline-debug.xml | 1 - data/swap/detekt-baseline-debug.xml | 19 ---- .../data/swap/DefaultSwapRepositoryV2.kt | 51 ++++----- .../swap/DefaultSwapTransactionRepository.kt | 28 ++--- .../data/swap/converter/SwapDataConverter.kt | 6 +- detekt_baseline_report.txt | 30 +++-- domain/staking/detekt-baseline-debug.xml | 1 - .../impl/detekt-baseline-debug.xml | 1 - .../markets/impl/detekt-baseline-debug.xml | 1 - .../impl/detekt-baseline-debug.xml | 2 - .../referral/impl/detekt-baseline-debug.xml | 1 - .../swap-v2/impl/detekt-baseline-debug.xml | 46 -------- .../analytics/SwapAmountAnalyticEvents.kt | 2 +- .../v2/impl/amount/entity/SwapAmountUM.kt | 4 +- .../v2/impl/amount/model/SwapAmountModel.kt | 106 +++++++++--------- .../SwapAmountPrimaryReadyStateTransformer.kt | 6 +- ...wapAmountSecondaryReadyStateTransformer.kt | 6 +- .../SwapAmountSelectQuoteTransformer.kt | 4 +- .../SwapAmountSetQuotesTransformer.kt | 10 +- .../impl/amount/ui/SwapAmountBlockContent.kt | 4 +- .../v2/impl/amount/ui/SwapAmountContent.kt | 10 +- .../ui/preview/SwapAmountContentPreview.kt | 8 +- .../model/SwapChooseProviderModel.kt | 14 +-- .../SwapProviderListItemConverter.kt | 6 +- .../converter/SwapProviderStateConverter.kt | 6 +- .../model/SwapChooseTokenNetworkModel.kt | 8 +- .../swap/v2/impl/common/SwapAlertFactory.kt | 3 +- .../swap/v2/impl/common/entity/ConfirmUM.kt | 2 +- .../analytics/SendWithSwapAnalyticEvents.kt | 2 +- .../confirm/model/SendWithSwapConfirmModel.kt | 33 +++--- .../confirm/model/SwapTransactionSender.kt | 10 +- ...dWithSwapConfirmInitialStateTransformer.kt | 2 +- ...wapConfirmationNotificationsTransformer.kt | 8 +- .../sendviaswap/model/SendWithSwapModel.kt | 10 +- .../success/ui/SendWithSwapSuccessContent.kt | 2 +- .../sendviaswap/ui/SendWithSwapContent.kt | 10 +- .../tester/impl/detekt-baseline-debug.xml | 2 - .../impl/detekt-baseline-debug.xml | 1 - .../wallet/impl/detekt-baseline-debug.xml | 1 - 39 files changed, 195 insertions(+), 272 deletions(-) delete mode 100644 data/swap/detekt-baseline-debug.xml delete mode 100644 features/swap-v2/impl/detekt-baseline-debug.xml diff --git a/core/ui/detekt-baseline-debug.xml b/core/ui/detekt-baseline-debug.xml index a625c6029f..662bc3318f 100644 --- a/core/ui/detekt-baseline-debug.xml +++ b/core/ui/detekt-baseline-debug.xml @@ -24,7 +24,6 @@ NoNameShadowing:TextAnimatedCounter.kt$char PropertyUsedBeforeDeclaration:InputManager.kt$InputManager$_query ReusedModifierInstance:EllipsisText.kt$Text( text = layoutText, color = color, style = style, fontStyle = fontStyle, textDecoration = textDecoration, textAlign = textAlign, softWrap = softWrap, maxLines = 1, onTextLayout = { textLayoutResultState.value = it }, modifier = modifier, ) - ReusedModifierInstance:Label.kt$Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier .padding(horizontal = 4.dp) .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( if (state.onClick != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = state.onClick, ) } else { Modifier }, ) .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), style = TangemTheme.typography.caption1, color = textColor, ) AnimatedVisibility(state.icon != null) { val wrappedIcon = remember(this) { requireNotNull(state.icon) } Icon( imageVector = ImageVector.vectorResource(wrappedIcon), tint = iconColor, contentDescription = null, modifier = Modifier .size(16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(bounded = false), onClick = { state.onIconClick?.invoke() }, ), ) } } ReusedModifierInstance:TangemRadioButton.kt$AnimatedVisibility( visible = isSelected, label = "Radio button animation", modifier = modifier .size(TangemTheme.dimens.size24), ) { Icon( painter = painterResource(id = R.drawable.ic_check_circle_24), contentDescription = null, tint = TangemTheme.colors.control.checked, ) } ReusedModifierInstance:TokenPrice.kt$Icon( modifier = modifier, painter = painterResource( id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, ) UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -> Unit diff --git a/data/swap/detekt-baseline-debug.xml b/data/swap/detekt-baseline-debug.xml deleted file mode 100644 index b047ac0eff..0000000000 --- a/data/swap/detekt-baseline-debug.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - MaxChainedCallsOnSameLine:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty() - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ Timber.w(it, "Unable to get pairs") throw it } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.from.contractAddress && it.currency.network.backendId == pair.from.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.to.contractAddress && it.currency.network.backendId == pair.to.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.from.contractAddress && it.network.backendId == pair.from.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.to.contractAddress && it.network.backendId == pair.to.network } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.userWalletId == userWallet.walletId.stringValue && ( it.toCryptoCurrencyId == cryptoCurrencyId.value || it.fromCryptoCurrencyId == cryptoCurrencyId.value ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ listConverter.convertBack( value = it, multiAccountList = multiAccountList, userWallet = userWallet, txStatuses = txStatuses, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ storeTransactionState( txId = transaction.txId, status = it, accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency, ) } - MultilineLambdaItParameter:SwapDataConverter.kt$SwapDataConverter${ if (it == "0") { BigDecimal.ZERO } else { requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } - NoNameShadowing:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$mappedProviders - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.txId == txId } - - diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 51c2a6700d..94583d0bee 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -79,23 +79,23 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, filterProviderTypes = filterProviderTypes, ) - val mappedProviders = providers.associateBy(ExpressProvider::providerId) + val expressProviders = providers.associateBy(ExpressProvider::providerId) allPairs.map { pair -> async { val statusFrom = cryptoCurrencyStatusList - .firstOrNull { - it.currency.getContractAddress() == pair.from.contractAddress && - it.currency.network.backendId == pair.from.network + .firstOrNull { currencyStatus -> + currencyStatus.currency.getContractAddress() == pair.from.contractAddress && + currencyStatus.currency.network.backendId == pair.from.network } val statusTo = cryptoCurrencyStatusList - .firstOrNull { - it.currency.getContractAddress() == pair.to.contractAddress && - it.currency.network.backendId == pair.to.network + .firstOrNull { currencyStatus -> + currencyStatus.currency.getContractAddress() == pair.to.contractAddress && + currencyStatus.currency.network.backendId == pair.to.network } val mappedProviders = pair.providers.mapNotNull { - mappedProviders[it.providerId] + expressProviders[it.providerId] }.filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { @@ -135,16 +135,16 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( async { val statusFromDeferred = async { cryptoCurrencyList - .firstOrNull { - it.getContractAddress() == pair.from.contractAddress && - it.network.backendId == pair.from.network + .firstOrNull { currency -> + currency.getContractAddress() == pair.from.contractAddress && + currency.network.backendId == pair.from.network } } val statusToDeferred = async { cryptoCurrencyList - .firstOrNull { - it.getContractAddress() == pair.to.contractAddress && - it.network.backendId == pair.to.network + .firstOrNull { currency -> + currency.getContractAddress() == pair.to.contractAddress && + currency.network.backendId == pair.to.network } } @@ -213,27 +213,27 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( expressOperationType: ExpressOperationType, ): SwapDataModel = withContext(coroutineDispatcher.io) { val requestId = UUID.randomUUID().toString() - val fromCryptoCurrency = fromCryptoCurrencyStatus.currency + val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus val refundData = when (expressProvider.type) { ExpressProviderType.CEX, ExpressProviderType.DEX_BRIDGE, ExpressProviderType.DEX, -> SwapRefundData( - refundAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value, + refundAddress = fromStatus.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null ) else -> null } val response = tangemExpressApi.getExchangeData( - fromContractAddress = fromCryptoCurrency.getContractAddress(), + fromContractAddress = fromCurrency.getContractAddress(), toContractAddress = toCryptoCurrency.getContractAddress(), - fromNetwork = fromCryptoCurrency.network.backendId, + fromNetwork = fromCurrency.network.backendId, toNetwork = toCryptoCurrency.network.backendId, - fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromStatus.networkAddress?.defaultAddress?.value.orEmpty(), toAddress = toAddress, - fromDecimals = fromCryptoCurrency.decimals, + fromDecimals = fromCurrency.decimals, toDecimals = toCryptoCurrency.decimals, fromAmount = fromAmount, providerId = expressProvider.providerId, @@ -277,6 +277,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( txHash: String, txExtraId: String?, ) { + val (currency, status) = fromCryptoCurrencyStatus withContext(coroutineDispatcher.io) { tangemExpressApi.exchangeSent( userWalletId = userWallet.walletId.stringValue, @@ -286,8 +287,8 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ), body = ExchangeSentRequestBody( txId = txId, - fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId, - fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromNetwork = currency.network.backendId, + fromAddress = status.networkAddress?.defaultAddress?.value.orEmpty(), payinAddress = toAddress, payinExtraId = txExtraId, txHash = txHash, @@ -364,9 +365,9 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ), ).getOrThrow() }, - onError = { - Timber.w(it, "Unable to get pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to get pairs") + throw error }, ) } diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index 0e2d2a1a79..cbcb56f715 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -64,10 +64,10 @@ internal class DefaultSwapTransactionRepository( toAccount: Account.CryptoPortfolio?, transaction: SwapTransactionModel, ) { - transaction.status?.let { + transaction.status?.let { swapTxList -> storeTransactionState( txId = transaction.txId, - status = it, + status = swapTxList, accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency, ) } @@ -76,8 +76,8 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ) val tokenTransactions = savedTransactions - ?.firstOrNull { - it.checkId( + ?.firstOrNull { swapTxList -> + swapTxList.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, @@ -129,17 +129,17 @@ internal class DefaultSwapTransactionRepository( }, ) { savedTransactions, txStatuses, multiAccountList -> val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWallet.walletId.stringValue && + ?.filter { swapTxList -> + swapTxList.userWalletId == userWallet.walletId.stringValue && ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value + swapTxList.toCryptoCurrencyId == cryptoCurrencyId.value || + swapTxList.fromCryptoCurrencyId == cryptoCurrencyId.value ) } - currencyTxs?.mapNotNull { + currencyTxs?.mapNotNull { swapTxList -> listConverter.convertBack( - value = it, + value = swapTxList, multiAccountList = multiAccountList, userWallet = userWallet, txStatuses = txStatuses, @@ -155,8 +155,8 @@ internal class DefaultSwapTransactionRepository( ) val tokenTransactions = savedList ?.asSequence() - ?.map { - it.copy(transactions = it.transactions.filterNot { it.txId == txId }) + ?.map { swapTxList -> + swapTxList.copy(transactions = swapTxList.transactions.filterNot { swapTx -> swapTx.txId == txId }) }?.filterNot { it.transactions.isEmpty() } ?.toList() @@ -257,8 +257,8 @@ internal class DefaultSwapTransactionRepository( toAccount = toAccount, tokenTransactions = transactions, ), - predicate = { - it.checkId( + predicate = { swapTxList -> + swapTxList.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt index 34f26aed27..0bac85ac2e 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt @@ -31,11 +31,11 @@ internal class SwapDataConverter : Converter + if (otherFee == "0") { BigDecimal.ZERO } else { - requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } + requireNotNull(otherFee.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } SwapDataTransactionModel.DEX( diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index d2f8545c7e..2822eda3e2 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-08 12:25:24 +Date: 2025-12-12 13:10:21 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1311 - Modules with Issues: 48 + Total Issues: 1247 + Modules with Issues: 46 Average Issues per Module: 27 Progress: - Fixed: 491 out of 1802 (27%) - Remaining: 1311 + Fixed: 555 out of 1802 (30%) + Remaining: 1247 ========================================== All Modules with Issues (sorted by count) @@ -31,21 +31,20 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/markets/impl 148 -features/wallet/impl 145 -features/onboarding-v2/impl 130 +features/markets/impl 147 +features/wallet/impl 144 +features/onboarding-v2/impl 128 features/swap/impl 67 data/wallet-connect 54 features/hot-wallet/impl 51 features/walletconnect/impl 47 -features/tokendetails/impl 46 features/staking/impl 46 -features/manage-tokens/impl 45 -features/swap-v2/impl 40 +features/tokendetails/impl 45 +features/manage-tokens/impl 44 domain/wallets 37 features/nft/impl 34 -features/tester/impl 30 -core/ui 27 +features/tester/impl 28 +core/ui 26 features/swap/domain 25 domain/tokens 25 common/ui 24 @@ -55,20 +54,19 @@ data/visa 21 data/nft 19 features/swap/data 15 data/wallets 13 -data/swap 13 features/token-recieve/impl 11 features/qr-scanning/impl 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 domain/transaction 8 +domain/staking 8 libs/tangem-sdk-api 7 +features/referral/impl 7 data/txhistory 7 features/welcome/impl 6 libs/visa 5 diff --git a/domain/staking/detekt-baseline-debug.xml b/domain/staking/detekt-baseline-debug.xml index 212e197fcc..ced29d17bf 100644 --- a/domain/staking/detekt-baseline-debug.xml +++ b/domain/staking/detekt-baseline-debug.xml @@ -2,7 +2,6 @@ - CanBeNonNullable:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$value: Any? MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress } NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId) diff --git a/features/manage-tokens/impl/detekt-baseline-debug.xml b/features/manage-tokens/impl/detekt-baseline-debug.xml index d580e5706e..6c3a7af5c8 100644 --- a/features/manage-tokens/impl/detekt-baseline-debug.xml +++ b/features/manage-tokens/impl/detekt-baseline-debug.xml @@ -15,7 +15,6 @@ MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) } MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) } MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) } - MultilineLambdaItParameter:CurrencyNetworksMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, onLongTap = onLongTap, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) } diff --git a/features/markets/impl/detekt-baseline-debug.xml b/features/markets/impl/detekt-baseline-debug.xml index 7c8b95b9d3..d9fe99460d 100644 --- a/features/markets/impl/detekt-baseline-debug.xml +++ b/features/markets/impl/detekt-baseline-debug.xml @@ -57,7 +57,6 @@ MultilineLambdaItParameter:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager${ when (val status = it.status) { is PaginationStatus.Paginating -> { if (status.lastResult is BatchFetchResult.Success) { it.data.size == 1 } else { null } } is PaginationStatus.EndOfPagination -> { it.data.size == 1 } else -> null } } MultilineLambdaItParameter:MarketsListItem.kt${ if (Random.nextBoolean()) { it.first.inc() to PriceChangeType.UP } else { it.first.dec() to PriceChangeType.DOWN } } MultilineLambdaItParameter:MarketsListLazyColumn.kt${ (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() ?.let { rawId -> CryptoCurrency.RawID(rawId) } } - MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it == BottomSheetState.EXPANDED) { analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it) { analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.isNotEmpty()) { activeListManager.getBatchKeysByItemIds(visibleItemIds.value) } else { null } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.list !is ListUM.Content) { visibleItemIds.value = emptyList() } } diff --git a/features/onboarding-v2/impl/detekt-baseline-debug.xml b/features/onboarding-v2/impl/detekt-baseline-debug.xml index 7d217909cf..1cc4bb2e21 100644 --- a/features/onboarding-v2/impl/detekt-baseline-debug.xml +++ b/features/onboarding-v2/impl/detekt-baseline-debug.xml @@ -98,7 +98,6 @@ MultilineLambdaItParameter:OnboardingVisaModel.kt$OnboardingVisaModel${ val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false VisaWalletPublicKeyUtility.validateExtendedPublicKey( targetAddress = targetAddress, extendedPublicKey = derivedKey, ).onLeft { return@any VisaWalletPublicKeyUtility.findKeyWithoutDerivation( targetAddress = targetAddress, card = wallet.scanResponse.card, ).isRight() }.isRight() } MultilineLambdaItParameter:OnboardingVisaOtherWalletModel.kt$OnboardingVisaOtherWalletModel${ if (it is VisaActivationRemoteState.AwaitingPinCode) { onDone.emit(it.activationOrderInfo) return@launch } } MultilineLambdaItParameter:OnboardingVisaOtherWalletModel.kt$OnboardingVisaOtherWalletModel${ uiMessageSender.showErrorDialog(it) analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(it)) delay(timeMillis = 60_000) } - MultilineLambdaItParameter:OnboardingVisaPinCodeModel.kt$OnboardingVisaPinCodeModel${ it.copy( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), error = if (isError) { analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation) resourceReference(R.string.visa_onboarding_pin_validation_error_message) } else { null }, ) } MultilineLambdaItParameter:OnboardingVisaWelcomeModel.kt$OnboardingVisaWelcomeModel${ onError(it) return@launch } MultilineLambdaItParameter:SeedPhraseCheckUiStateBuilder.kt$SeedPhraseCheckUiStateBuilder${ checkWordField( word = it.word.text, shownIndex = it.index, ) } MultilineLambdaItParameter:TwinWalletArtwork.kt${ animationState = it val maxTime = maxOf( transition1.totalDurationNanos, transition2.totalDurationNanos, ) delay(TimeUnit.NANOSECONDS.toMillis(maxTime)) } @@ -126,7 +125,6 @@ UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Backup$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.CreateWallet$mapOf() - UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Topup$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Twins$mapOf() UseEmptyCounterpart:OnboardingVisaAnalyticsEvent.kt$OnboardingVisaAnalyticsEvent$mapOf() UseEmptyCounterpart:VisaAnalyticsEvent.kt$VisaAnalyticsEvent$mapOf() diff --git a/features/referral/impl/detekt-baseline-debug.xml b/features/referral/impl/detekt-baseline-debug.xml index da51d44c59..9f594c6c2e 100644 --- a/features/referral/impl/detekt-baseline-debug.xml +++ b/features/referral/impl/detekt-baseline-debug.xml @@ -3,7 +3,6 @@ MultilineLambdaItParameter:AgreementText.kt${ val clickableSpanStyle = requireNotNull(agreementText.spanStyles.getOrNull(1)) if (it in clickableSpanStyle.start..clickableSpanStyle.end) { onClick() } } - MultilineLambdaItParameter:ReferralModel.kt$ReferralModel${ analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) referralData.value = it } 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), ) diff --git a/features/swap-v2/impl/detekt-baseline-debug.xml b/features/swap-v2/impl/detekt-baseline-debug.xml deleted file mode 100644 index d76a6dfe29..0000000000 --- a/features/swap-v2/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - BooleanPropertyNaming:ConfirmUM.kt$ConfirmUM.Content$val showTapHelp: Boolean - BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$private var showBestRateAnimation: Boolean = false - BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - BooleanPropertyNaming:SwapAmountPrimaryReadyStateTransformer.kt$SwapAmountPrimaryReadyStateTransformer$private val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountSecondaryReadyStateTransformer.kt$SwapAmountSecondaryReadyStateTransformer$private val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountSelectQuoteTransformer.kt$SwapAmountSelectQuoteTransformer$private val needApplyFCARestrictions: Boolean - BooleanPropertyNaming:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer$private val needApplyFcaRestrictions: Boolean - BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showFCAWarning: Boolean - BooleanPropertyNaming:SwapChooseProviderModel.kt$SwapChooseProviderModel$private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() - BooleanPropertyNaming:SwapProviderListItemConverter.kt$SwapProviderListItemConverter$private val needApplyFCARestrictions: Boolean - BooleanPropertyNaming:SwapProviderStateConverter.kt$SwapProviderStateConverter$private val needApplyFCARestrictions: Boolean - CouldBeSequence:SwapAmountModel.kt$SwapAmountModel$filter { it.currencyStatus.currency.id == toCryptoCurrency.id } - MaxChainedCallsOnSameLine:SwapAlertFactory.kt$SwapAlertFactory$confirmData?.fee?.amount?.value?.toString().orEmpty() - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ it.copy( confirmUM = SendWithSwapConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_privacy_policy), link = it, ) } - MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_terms_of_use), link = it, ) } - MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ Timber.w(it.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { modelScope.launch { swapAlertFactory.onFailedTxEmailClick( userWallet = userWallet, cryptoCurrency = params.currency, errorMessage = it.toString(), ) } }, popBack = ::onBackClick, ) } - MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ it.copy( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, ) } - MultilineLambdaItParameter:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } - MultilineLambdaItParameter:SwapChooseProviderModel.kt$SwapChooseProviderModel${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } - MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e("Failed to get user wallet: $it") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } - MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e(it.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, onDismiss = params.onDismiss, ), ) return@launch } - MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ Timber.e(it, "Failed to create swap CEX tx data") onSendError(SendTransactionError.UnknownError(Exception(it))) return } - MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ onExpressError(it); return } - 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 - NullableToStringCall:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$error.toString() - NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$primaryStatus - NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$secondaryStatus - PropertyUsedBeforeDeclaration:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$amountUM - UnnecessaryEventHandlerParameter:SwapAmountContent.kt$onExpandEditField: (SwapAmountType) -> Unit - UseEmptyCounterpart:SendWithSwapAnalyticEvents.kt$SendWithSwapAnalyticEvents$mapOf() - UseEmptyCounterpart:SendWithSwapSuccessContent.kt$listOf() - UseEmptyCounterpart:SwapAmountAnalyticEvents.kt$SwapAmountAnalyticEvents$mapOf() - VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency - VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var userWallet = params.userWallet - - diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt index 197bd8a27c..4dd7c9562d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER internal sealed class SwapAmountAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { data class ProviderSelectorClicked( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 62eb8a0d3b..4cbc2867a6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -48,11 +48,11 @@ internal sealed class SwapAmountUM { val swapCurrencies: SwapCurrencies, val swapQuotes: ImmutableList, val selectedQuote: SwapQuoteUM, - val showFCAWarning: Boolean, + val isShowFCAWarning: Boolean, // extra data val appCurrency: AppCurrency?, - val showBestRateAnimation: Boolean, + val isShowBestRateAnimation: Boolean, ) : SwapAmountUM() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index da46d20b3f..8546a9c6e2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -26,11 +26,8 @@ import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.swap.models.SwapCurrencies -import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection -import com.tangem.domain.swap.models.SwapQuoteModel -import com.tangem.domain.swap.models.getGroupWithDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase @@ -99,9 +96,9 @@ internal class SwapAmountModel @Inject constructor( private val params: SwapAmountComponentParams = paramsContainer.require() private val swapDirection = params.swapDirection private var appCurrency = AppCurrency.Default - private var userWallet = params.userWallet + private val userWallet = params.userWallet - private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency + private val primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() @@ -111,7 +108,7 @@ internal class SwapAmountModel @Inject constructor( private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private var showBestRateAnimation: Boolean = false + private var isShowBestRateAnimation: Boolean = false val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -124,7 +121,7 @@ internal class SwapAmountModel @Inject constructor( appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - showBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() + isShowBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() @@ -169,7 +166,7 @@ internal class SwapAmountModel @Inject constructor( quoteUM = quoteUM, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ), ) } @@ -270,7 +267,7 @@ internal class SwapAmountModel @Inject constructor( override fun onSelectTokenClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return modelScope.launch { - val showSendViaSwapNotification = shouldShowNotificationUseCase( + val isShowSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true @@ -281,7 +278,7 @@ internal class SwapAmountModel @Inject constructor( initialCurrency = primaryCryptoCurrency, selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, - shouldShowSendViaSwapNotification = showSendViaSwapNotification, + shouldShowSendViaSwapNotification = isShowSendViaSwapNotification, analyticsCategoryName = params.analyticsCategoryName, ), ) @@ -306,7 +303,7 @@ internal class SwapAmountModel @Inject constructor( fun onFinishAnimation() { uiState.update { - (it as? SwapAmountUM.Content)?.copy(showBestRateAnimation = false) ?: it + (it as? SwapAmountUM.Content)?.copy(isShowBestRateAnimation = false) ?: it } } @@ -367,7 +364,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, @@ -418,7 +415,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = isAccountsMode, account = account, @@ -532,7 +529,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this@SwapAmountModel, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, @@ -540,6 +537,7 @@ internal class SwapAmountModel @Inject constructor( ) startLoadingQuotesTask(isSilentReload = false) } else { + @Suppress("NullableToStringCall") Timber.e( """ Invalid cryptocurrencies status: @@ -596,50 +594,50 @@ internal class SwapAmountModel @Inject constructor( val isAmountScreen = params is SwapAmountComponentParams.AmountParams val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero() if (isAmountScreen && isAmountError) { - uiState.transformerUpdate(SwapQuoteEmptyStateTransformer) - return + uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return } if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { - val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available.filter { - it.currencyStatus.currency.id == toCryptoCurrency.id - }.flatMap { - it.providers - }.map { provider -> - async { - getSwapQuoteUseCase( - userWallet = userWallet, - fromCryptoCurrency = fromCryptoCurrency, - toCryptoCurrency = toCryptoCurrency, - fromAmount = fromAmountValue, - provider = provider, - ).fold( - ifLeft = { error -> - SwapQuoteUM.Error( - provider = provider, - expressError = error, - ) - }, - ifRight = { quote: SwapQuoteModel -> - SwapQuoteUMConverter( - primaryCurrency = fromCryptoCurrency, - secondaryCurrency = toCryptoCurrency, - swapDirection = swapDirection, - allowanceContract = quote.allowanceContract, - isApprovalNeeded = checkAllowance(state, quote), - fromAmount = fromAmountValue, - ).convert( - SwapQuoteUMConverter.Data( - quote = quote, + val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available + .asSequence() + .filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id } + .flatMap(SwapCryptoCurrency::providers) + .toList() + .map { provider -> + async { + getSwapQuoteUseCase( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmountValue, + provider = provider, + ).fold( + ifLeft = { error -> + SwapQuoteUM.Error( provider = provider, - ), - ) - }, - ) - } - }.awaitAll() + expressError = error, + ) + }, + ifRight = { quote: SwapQuoteModel -> + SwapQuoteUMConverter( + primaryCurrency = fromCryptoCurrency, + secondaryCurrency = toCryptoCurrency, + swapDirection = swapDirection, + allowanceContract = quote.allowanceContract, + isApprovalNeeded = checkAllowance(state, quote), + fromAmount = fromAmountValue, + ).convert( + SwapQuoteUMConverter.Data( + quote = quote, + provider = provider, + ), + ) + }, + ) + } + }.awaitAll() uiState.transformerUpdate( SwapAmountSetQuotesTransformer( @@ -647,7 +645,7 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, - needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), + isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), ), ) feeSelectorReloadTrigger.triggerUpdate() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index 9d39c5e8f4..ca25cc3fda 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -24,7 +24,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, - private val showBestRateAnimation: Boolean, + private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, private val account: Account.CryptoPortfolio?, @@ -60,8 +60,8 @@ internal class SwapAmountPrimaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, - showBestRateAnimation = showBestRateAnimation, - showFCAWarning = false, + isShowBestRateAnimation = isShowBestRateAnimation, + isShowFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 7d4a258917..298b11ad75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -25,7 +25,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, - private val showBestRateAnimation: Boolean, + private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, private val account: Account.CryptoPortfolio?, @@ -59,8 +59,8 @@ internal class SwapAmountSecondaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, - showBestRateAnimation = showBestRateAnimation, - showFCAWarning = false, + isShowBestRateAnimation = isShowBestRateAnimation, + isShowFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index cd0df36880..03058c869b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -19,7 +19,7 @@ internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -31,7 +31,7 @@ internal class SwapAmountSelectQuoteTransformer( return prevState.copy( isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, selectedQuote = quoteUM, - showFCAWarning = needApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content val amountField = swapAmountField?.amountField as? AmountState.Data diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 941fa7e8ec..cd19623928 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -22,14 +22,14 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, - private val needApplyFcaRestrictions: Boolean, + private val isNeedApplyFcaRestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val isSingleProvider = quotes.filter { - it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || - (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + val isSingleProvider = quotes.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError }.isSingleItem() val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) @@ -49,7 +49,7 @@ internal class SwapAmountSetQuotesTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, - needApplyFCARestrictions = needApplyFcaRestrictions && + isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && selectedQuote.provider?.isRestrictedByFCA() == true, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index 5fc6dc2696..008e402a09 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -88,7 +88,7 @@ internal fun SwapAmountBlockContent( SwapChooseProviderContent( isBestRate = isBestRate, isSingleProvider = quoteContent?.isSingleProvider == true, - showBestRateAnimation = amountUM.showBestRateAnimation, + showBestRateAnimation = amountUM.isShowBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, onFinishAnimation = onFinishAnimation, @@ -98,7 +98,7 @@ internal fun SwapAmountBlockContent( start.linkTo(parent.start) end.linkTo(parent.end) }, - showFCAWarning = amountUM.showFCAWarning, + showFCAWarning = amountUM.isShowFCAWarning, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 91c6f69e30..61ec2095f4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -175,7 +175,7 @@ private fun SwapAmountBlock( selectedQuote = selectedQuote, isSelectedAmountType = isSelectedAmountType, isFixedRate = isFixedRate, - onExpandEditField = clickIntents::onExpandEditField, + onExpandEditField = { clickIntents.onExpandEditField(amountFieldUM.amountType) }, onSelectTokenClick = clickIntents::onSelectTokenClick, onMaxAmountClick = clickIntents::onMaxValueClick, ) @@ -195,14 +195,14 @@ private fun SwapAmountEditBlock( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier.padding(top = 48.dp, bottom = 28.dp), ) { - when (val amountFieldUM = amountFieldUM.amountField) { + when (val amountField = amountFieldUM.amountField) { !is AmountState.Data -> { TextShimmer( style = TangemTheme.typography.caption2, modifier = Modifier.width(60.dp), ) } - else -> AccountTitle(amountFieldUM.accountTitleUM) + else -> AccountTitle(amountField.accountTitleUM) } AmountFieldV2( amountUM = amountFieldUM.amountField, @@ -221,7 +221,7 @@ private fun SwapAmountInfo( selectedQuote: SwapQuoteUM?, isSelectedAmountType: Boolean, isFixedRate: Boolean, - onExpandEditField: (SwapAmountType) -> Unit, + onExpandEditField: () -> Unit, onMaxAmountClick: () -> Unit, onSelectTokenClick: () -> Unit, modifier: Modifier = Modifier, @@ -237,7 +237,7 @@ private fun SwapAmountInfo( enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, onClick = { if (isFixedRate) { - onExpandEditField(amountFieldUM.amountType) + onExpandEditField() } else { onSelectTokenClick() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 01c4c02b06..798b7fd4d0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -90,8 +90,8 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, - showBestRateAnimation = false, - showFCAWarning = false, + isShowBestRateAnimation = false, + isShowFCAWarning = false, ) val defaultState = SwapAmountUM.Content( @@ -129,8 +129,8 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, - showBestRateAnimation = false, - showFCAWarning = true, + isShowBestRateAnimation = false, + isShowFCAWarning = true, ) val defaultStateAccount: SwapAmountUM.Content diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index 0ac41aea88..dc784cc913 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -25,13 +25,13 @@ internal class SwapChooseProviderModel @Inject constructor( private val params: SwapChooseProviderComponent.Params = paramsContainer.require() - private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() + private val isNeedApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { SwapProviderListItemConverter( cryptoCurrency = params.cryptoCurrency, selectedProvider = params.selectedProvider, - needApplyFCARestrictions = needApplyFCARestrictions, + isNeedApplyFCARestrictions = isNeedApplyFCARestrictions, needBestRateBadge = params.providers.filterIsInstance().isSingleItem().not(), ) } @@ -45,13 +45,13 @@ internal class SwapChooseProviderModel @Inject constructor( } private fun getInitialState(): SwapChooseProviderBottomSheetContent { - val filteredProviderList = params.providers.filter { - it is SwapQuoteUM.Content || - it is SwapQuoteUM.Allowance || - (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + val filteredProviderList = params.providers.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || + swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } return SwapChooseProviderBottomSheetContent( - isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), + isApplyFCARestrictions = isNeedApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), providerList = swapProviderListItemConverter.convertList(filteredProviderList) .filterNotNull() .toPersistentList(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 045d882f8f..56c7a0f93a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -21,14 +21,14 @@ import com.tangem.utils.converter.Converter internal class SwapProviderListItemConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, needBestRateBadge: Boolean, ) : Converter { private val providerStateConverter = SwapProviderStateConverter( cryptoCurrency = cryptoCurrency, selectedProvider = selectedProvider, - needApplyFCARestrictions = needApplyFCARestrictions, + isNeedApplyFCARestrictions = isNeedApplyFCARestrictions, isNeedBestRateBadge = needBestRateBadge, ) @@ -78,7 +78,7 @@ internal class SwapProviderListItemConverter( }, ) } - is SwapQuoteUM.Content -> if (needApplyFCARestrictions && value.provider.isRestrictedByFCA()) { + is SwapQuoteUM.Content -> if (isNeedApplyFCARestrictions && value.provider.isRestrictedByFCA()) { ProviderChooseUM.ExtraUM.Action( text = resourceReference(R.string.express_provider_fca_warning_list), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt index 317f0d733f..54f3030f4f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -20,7 +20,7 @@ internal class SwapProviderStateConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, private val isNeedBestRateBadge: Boolean, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, ) : Converter { override fun convert(value: SwapQuoteUM): SwapProviderState { @@ -41,7 +41,7 @@ internal class SwapProviderStateConverter( } val additionalBadge = when { - needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList isNeedBestRateBadge && isBestRate -> AdditionalBadge.BestTrade else -> AdditionalBadge.Empty } @@ -59,7 +59,7 @@ internal class SwapProviderStateConverter( private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState { val additionalBadge = when { - needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList else -> AdditionalBadge.Empty } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index 8d2c61caf1..a9312fc84a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -66,8 +66,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( } private fun initContent() { - val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { - Timber.e("Failed to get user wallet: $it") + val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { error -> + Timber.e("Failed to get user wallet: $error") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } @@ -87,8 +87,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, swapTxType = SwapTxType.SendWithSwap, - ).getOrElse { - Timber.e(it.toString()) + ).getOrElse { error -> + Timber.e(error.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index d5360e2add..0994844d9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -91,7 +91,8 @@ internal class SwapAlertFactory @Inject constructor( destinationAddress = confirmData?.enteredDestination.orEmpty(), tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(), amount = confirmData?.enteredAmount?.toString().orEmpty(), - fee = confirmData?.fee?.amount?.value?.toString().orEmpty(), + fee = confirmData?.fee?.amount?.value?.toString() + .orEmpty(), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index b105bed8a1..cf27f09d02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -15,7 +15,7 @@ internal sealed class ConfirmUM { data class Content( override val isPrimaryButtonEnabled: Boolean = false, val isTransactionInProcess: Boolean, - val showTapHelp: Boolean, + val isShowTapHelp: Boolean, val sendingFooter: TextReference, val notifications: ImmutableList, val tosUM: TosUM?, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index ab38425164..e7e6ac5e7d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -13,7 +13,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents internal sealed class SendWithSwapAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { data class TransactionScreenOpened( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index c0d119b50f..3fcd6d870c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -94,11 +94,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( field = MutableStateFlow(params.sendWithSwapUM) val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value - val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value - val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { - "Crypto currency must not be null" - } private val swapTransactionSender = swapTransactionSenderFactory.create(params.userWallet) @@ -111,6 +107,11 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeSelectorUM get() = uiState.value.feeSelectorUM as? FeeSelectorUM.Content + val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus + val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { + "Crypto currency must not be null" + } + val confirmData: ConfirmData get() { val amountUM = amountUM @@ -270,13 +271,13 @@ internal class SendWithSwapConfirmModel @Inject constructor( uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) swapAlertFactory.getSendTransactionErrorState( error = error, - onFailedTxEmailClick = { + onFailedTxEmailClick = { _ -> modelScope.launch { swapAlertFactory.onFailedTxEmailClick( userWallet = params.userWallet, cryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency, confirmData = confirmData, - errorMessage = error.toString(), + errorMessage = error?.toString().orEmpty(), ) } }, @@ -320,8 +321,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty) { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( confirmUM = SendWithSwapConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, ).transform(uiState.value.confirmUM), @@ -335,9 +336,9 @@ internal class SendWithSwapConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -376,12 +377,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( flow2 = swapNotificationsUpdateListener.hasErrorFlow, ) { hasSendError, hasSwapError -> val hasError = hasSendError || hasSwapError - uiState.update { - val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + uiState.update { state -> + val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } }.launchIn(modelScope) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 3602881267..1c6a439eeb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -98,8 +98,8 @@ internal class SwapTransactionSender @AssistedInject constructor( toAddress = destination, expressProvider = provider, rateType = rateType, - expressOperationType, - ).getOrElse { onExpressError(it); return } + expressOperationType = expressOperationType, + ).getOrElse { error -> onExpressError(error); return } createAndSendCexTransaction( fromAmount = fromAmount, @@ -141,9 +141,9 @@ internal class SwapTransactionSender @AssistedInject constructor( destination = swapTransaction.txTo, userWalletId = userWallet.walletId, network = fromStatus.currency.network, - ).getOrElse { - Timber.e(it, "Failed to create swap CEX tx data") - onSendError(SendTransactionError.UnknownError(Exception(it))) + ).getOrElse { error -> + Timber.e(error, "Failed to create swap CEX tx data") + onSendError(SendTransactionError.UnknownError(Exception(error))) return } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt index 64e34d1673..1beaabe312 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -12,7 +12,7 @@ internal class SendWithSwapConfirmInitialStateTransformer( return ConfirmUM.Content( isPrimaryButtonEnabled = false, isTransactionInProcess = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), tosUM = null, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt index 3176e70a6c..53fd2df373 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -109,16 +109,16 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_terms_of_use), - link = it, + link = termsOfUse, ) }, - policyLink = expressProvider.privacyPolicy?.let { + policyLink = expressProvider.privacyPolicy?.let { privacyPolicy -> ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_privacy_policy), - link = it, + link = privacyPolicy, ) }, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 3d6968a554..629d37898d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -132,8 +132,8 @@ internal class SendWithSwapModel @Inject constructor( } override fun resetSendWithSwapNavigation(resetNavigation: Boolean) { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, @@ -166,8 +166,8 @@ internal class SendWithSwapModel @Inject constructor( userWallet = wallet getPrimaryCurrencyStatusUpdates(params.currency) }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + Timber.w(error.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { @@ -175,7 +175,7 @@ internal class SendWithSwapModel @Inject constructor( swapAlertFactory.onFailedTxEmailClick( userWallet = userWallet, cryptoCurrency = params.currency, - errorMessage = it.toString(), + errorMessage = error.toString(), ) } }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 0ccf5aada7..c106e29968 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -344,7 +344,7 @@ private fun SendWithSwapSuccessContent_Preview() { txUrl = "https://tangem.com", provider = ExpressProvider( providerId = "changelly", - rateTypes = listOf(), + rateTypes = emptyList(), name = "Changelly", type = ExpressProviderType.CEX, imageLarge = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changelly-1024.png", diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index fe0e00831b..9e3064dafc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -36,7 +36,7 @@ internal fun SendWithSwapContent( stackState: ChildStack, onLinkClick: (String) -> Unit, ) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return + val navigationUMContent = navigationUM as? NavigationUM.Content ?: return Column( modifier = Modifier @@ -47,9 +47,9 @@ internal fun SendWithSwapContent( horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarWithBackButton( - text = navigationUM.title.resolveReference(), - onBackClick = navigationUM.backIconClick, - iconRes = navigationUM.additionalIconRes, + text = navigationUMContent.title.resolveReference(), + onBackClick = navigationUMContent.backIconClick, + iconRes = navigationUMContent.additionalIconRes, modifier = Modifier.height(TangemTheme.dimens.size56), ) Children( @@ -93,7 +93,7 @@ internal fun SendWithSwapContent( ) } NavigationPrimaryButton( - navigationUM.primaryButton, + navigationUMContent.primaryButton, modifier = Modifier.padding( start = 16.dp, end = 16.dp, diff --git a/features/tester/impl/detekt-baseline-debug.xml b/features/tester/impl/detekt-baseline-debug.xml index d115b8f9b6..be0960afae 100644 --- a/features/tester/impl/detekt-baseline-debug.xml +++ b/features/tester/impl/detekt-baseline-debug.xml @@ -22,12 +22,10 @@ MultilineLambdaItParameter:TesterAccountsViewModel.kt$TesterAccountsViewModel${ it.copy( accountListBottomSheetConfig = it.accountListBottomSheetConfig.copy( isAccountsShown = false, ), ) } MultilineLambdaItParameter:TesterAccountsViewModel.kt$TesterAccountsViewModel${ it.copy( walletSelector = it.walletSelector.copy(selected = userWallet), accountListBottomSheetConfig = it.accountListBottomSheetConfig.copy( accounts = getWalletAccounts(userWallet.walletId), ), ) } MultilineLambdaItParameter:TesterActionsScreen.kt${ builder.setStream( FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it), ) } - MultilineLambdaItParameter:TesterActivity.kt$TesterActivity${ val route = when (it) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS } innerTesterRouter.open(route) } NoNameShadowing:AccountsScreen.kt$content NoNameShadowing:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ onAddProviderClick(id = blockchain.id, url = it) } NoNameShadowing:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ onPublicProviderUrlChange(id = blockchain.id, url = it) } NoNameShadowing:EnvironmentsTogglesViewModel.kt$EnvironmentsTogglesViewModel${ it.environment == currentEnvironment } - NoNameShadowing:TesterActivity.kt$TesterActivity${ val route = when (it) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS } innerTesterRouter.open(route) } PropertyUsedBeforeDeclaration:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel$_state PropertyUsedBeforeDeclaration:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel$searchQuery PropertyUsedBeforeDeclaration:EnvironmentsTogglesViewModel.kt$EnvironmentsTogglesViewModel$_uiState diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index f45527c13b..71ab124fb2 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -24,7 +24,6 @@ MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) } MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) } MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ val updatedState = stateFactory.getStateWithNotifications(it) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } - MultilineLambdaItParameter:TokenDetailsOnrampTransactionStateConverter.kt$TokenDetailsOnrampTransactionStateConverter${ analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) } MultilineLambdaItParameter:TokenDetailsScreen.kt${ Notification( modifier = itemModifier.animateItem(), config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) } MultilineLambdaItParameter:TokenDetailsTopAppBar.kt${ TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TokenStakingBlock.kt${ when (it) { is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock() is StakingBlockUM.Loading -> StakingLoading() is StakingBlockUM.Staked -> StakingBalanceBlock( state = it, isBalanceHidden = isBalanceHidden, ) is StakingBlockUM.StakeAvailable -> StakingAvailableContent( state = it, ) } } diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index 5a9fa2634b..f3a1111f2a 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -45,7 +45,6 @@ MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -> when (button) { is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } } MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } - MultilineLambdaItParameter:SingleWalletOnrampTransactionConverter.kt$SingleWalletOnrampTransactionConverter${ analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) } MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } } MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } } MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryCountErrorTransformer( userWallet = userWallet, error = it, pendingTransactions = status.value.pendingTransactions, clickIntents = clickIntents, ) } From b316ef4d87e9b396e0d28d66f738c89249158108 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 14:22:30 +0200 Subject: [PATCH 065/130] Updated on 2026-08-14 --- .../analytics/models/detekt-baseline-main.xml | 17 ++++++++ core/pagination/detekt-baseline-main.xml | 22 ++++++++++ core/utils/detekt-baseline-main.xml | 14 +++++++ detekt_baseline_report.txt | 40 +++++++++++++++---- domain/account/detekt-baseline-main.xml | 12 ++++++ .../balance-hiding/detekt-baseline-main.xml | 9 +++++ domain/core/detekt-baseline-main.xml | 11 +++++ .../express/models/detekt-baseline-main.xml | 7 ++++ .../feedback/models/detekt-baseline-main.xml | 7 ++++ .../markets/models/detekt-baseline-main.xml | 7 ++++ domain/models/detekt-baseline-main.xml | 27 +++++++++++++ domain/networks/detekt-baseline-main.xml | 8 ++++ domain/nft/models/detekt-baseline-main.xml | 9 +++++ domain/onboarding/detekt-baseline-main.xml | 7 ++++ domain/onramp/detekt-baseline-main.xml | 12 ++++++ domain/onramp/models/detekt-baseline-main.xml | 10 +++++ .../domain/onramp/GetOnrampOffersUseCase.kt | 6 +-- domain/promo/detekt-baseline-main.xml | 7 ++++ domain/quotes/detekt-baseline-main.xml | 7 ++++ .../staking/models/detekt-baseline-main.xml | 19 +++++++++ domain/tokens/models/detekt-baseline-main.xml | 13 ++++++ .../models/detekt-baseline-main.xml | 7 ++++ .../txhistory/models/detekt-baseline-main.xml | 8 ++++ domain/visa/models/detekt-baseline-main.xml | 14 +++++++ .../models/detekt-baseline-main.xml | 12 ++++++ .../models/detekt-baseline-main.xml | 5 +++ .../wallets/models/detekt-baseline-main.xml | 7 ++++ .../models/detekt-baseline-main.xml | 7 ++++ fastlane/Fastfile | 2 +- .../configurations/DetektConfigurations.kt | 25 ++++++++++-- tangem-android-tools | 2 +- test/mock/detekt-baseline-main.xml | 7 ++++ update_detekt_baseline.sh | 12 +++--- 33 files changed, 358 insertions(+), 21 deletions(-) create mode 100644 core/analytics/models/detekt-baseline-main.xml create mode 100644 core/pagination/detekt-baseline-main.xml create mode 100644 core/utils/detekt-baseline-main.xml create mode 100644 domain/account/detekt-baseline-main.xml create mode 100644 domain/balance-hiding/detekt-baseline-main.xml create mode 100644 domain/core/detekt-baseline-main.xml create mode 100644 domain/express/models/detekt-baseline-main.xml create mode 100644 domain/feedback/models/detekt-baseline-main.xml create mode 100644 domain/markets/models/detekt-baseline-main.xml create mode 100644 domain/models/detekt-baseline-main.xml create mode 100644 domain/networks/detekt-baseline-main.xml create mode 100644 domain/nft/models/detekt-baseline-main.xml create mode 100644 domain/onboarding/detekt-baseline-main.xml create mode 100644 domain/onramp/detekt-baseline-main.xml create mode 100644 domain/onramp/models/detekt-baseline-main.xml create mode 100644 domain/promo/detekt-baseline-main.xml create mode 100644 domain/quotes/detekt-baseline-main.xml create mode 100644 domain/staking/models/detekt-baseline-main.xml create mode 100644 domain/tokens/models/detekt-baseline-main.xml create mode 100644 domain/transaction/models/detekt-baseline-main.xml create mode 100644 domain/txhistory/models/detekt-baseline-main.xml create mode 100644 domain/visa/models/detekt-baseline-main.xml create mode 100644 domain/wallet-connect/models/detekt-baseline-main.xml create mode 100644 domain/wallet-manager/models/detekt-baseline-main.xml create mode 100644 domain/wallets/models/detekt-baseline-main.xml create mode 100644 domain/yield-supply/models/detekt-baseline-main.xml create mode 100644 test/mock/detekt-baseline-main.xml diff --git a/core/analytics/models/detekt-baseline-main.xml b/core/analytics/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c8cb8b3d13 --- /dev/null +++ b/core/analytics/models/detekt-baseline-main.xml @@ -0,0 +1,17 @@ + + + + + MultilineLambdaItParameter:TechAnalyticsEvent.kt$TechAnalyticsEvent.KeyboardIdentifier${ put("Package", it) put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName") } + UseEmptyCounterpart:AnalyticsEvent.kt$AnalyticsEvent$mapOf() + UseEmptyCounterpart:Basic.kt$Basic$mapOf() + UseEmptyCounterpart:ExceptionAnalyticsEvent.kt$ExceptionAnalyticsEvent$mapOf() + UseEmptyCounterpart:MainScreenAnalyticsEvent.kt$MainScreenAnalyticsEvent$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.CreateWallet$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Error$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Onboarding$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.SeedPhrase$mapOf() + UseEmptyCounterpart:TechAnalyticsEvent.kt$TechAnalyticsEvent$mapOf() + + diff --git a/core/pagination/detekt-baseline-main.xml b/core/pagination/detekt-baseline-main.xml new file mode 100644 index 0000000000..5257c3fbcb --- /dev/null +++ b/core/pagination/detekt-baseline-main.xml @@ -0,0 +1,22 @@ + + + + + BooleanPropertyNaming:BatchAction.kt$BatchAction.UpdateBatches$val async: Boolean = false + BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val empty: Boolean + BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val last: Boolean + BooleanPropertyNaming:BatchListSource.kt$DefaultBatchListSource$val started = job.start() + MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } + MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ if (predicate(it.first)) { it.second.cancel() null } else { it } } + MultilineLambdaItParameter:CursorBatchFetcher.kt$CursorBatchFetcher${ currentCoroutineContext().ensureActive() return BatchFetchResult.Error(it) } + MultilineLambdaItParameter:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } + NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) + NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) + NestedScopeFunctions:BatchListSource.kt$DefaultBatchListSource$also { currentCoroutineContext().ensureActive() } + SuspendFunSwallowedCancellation:BatchListSource.kt$DefaultBatchListSource$runCatching + SuspendFunSwallowedCancellation:CursorBatchFetcher.kt$CursorBatchFetcher$runCatching + SuspendFunSwallowedCancellation:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher$runCatching + UseEmptyCounterpart:BatchListSource.kt$DefaultBatchListSource$listOf() + UseOrEmpty:BatchListSource.kt$DefaultBatchListSource$batch?.let { listOf(it) } ?: emptyList() + + diff --git a/core/utils/detekt-baseline-main.xml b/core/utils/detekt-baseline-main.xml new file mode 100644 index 0000000000..82dd627f26 --- /dev/null +++ b/core/utils/detekt-baseline-main.xml @@ -0,0 +1,14 @@ + + + + + BooleanPropertyNaming:Retryer.kt$Retryer$val result = try { block(iteration) } catch (e: CancellationException) { throw e } catch (e: Error) { throw e } catch (_: Exception) { false } + MultilineLambdaItParameter:Converter.kt$Converter${ try { convert(it) } catch (throwable: Throwable) { onError?.invoke(throwable) null } } + MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onFailure } onError.invoke(it) } + MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onSuccess } onSuccess.invoke(it) } + NullableBooleanCheck:JobHolder.kt$JobHolder$job?.isActive ?: false + PropertyUsedBeforeDeclaration:JobHolder.kt$JobHolder$job + SuspendFunSwallowedCancellation:CoroutineExt.kt$runCatching + VarCouldBeVal:PeriodicTask.kt$PeriodicTask$private var isActive: AtomicBoolean = AtomicBoolean(false) + + diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 2822eda3e2..a45e2bff61 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,13 +1,13 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-12 13:10:21 +Date: 2025-12-12 13:17:20 Step 1: Running detekt to check for new issues... ✓ Detekt passed - no new issues found -Step 2: Updating detekt baseline for debug variant... +Step 2: Updating detekt baseline ... Baseline updated successfully! @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1247 - Modules with Issues: 46 - Average Issues per Module: 27 + Total Issues: 1378 + Modules with Issues: 72 + Average Issues per Module: 19 Progress: - Fixed: 555 out of 1802 (30%) - Remaining: 1247 + Fixed: 555 out of 1933 (28%) + Remaining: 1378 ========================================== All Modules with Issues (sorted by count) @@ -50,31 +50,57 @@ domain/tokens 25 common/ui 24 features/yield-supply/impl 21 features/tangempay/details/impl 21 +domain/models 21 data/visa 21 data/nft 19 +core/pagination 16 features/swap/data 15 +domain/staking/models 13 data/wallets 13 features/token-recieve/impl 11 features/qr-scanning/impl 11 data/onramp 11 core/datasource 11 +core/analytics/models 11 data/markets 10 features/details/impl 9 domain/account/status 9 data/yield-supply 9 data/networks 9 +domain/visa/models 8 domain/transaction 8 domain/staking 8 +core/utils 8 libs/tangem-sdk-api 7 features/referral/impl 7 +domain/tokens/models 7 data/txhistory 7 features/welcome/impl 6 +domain/wallet-connect/models 6 +domain/onramp 6 +domain/account 6 libs/visa 5 features/home/impl 5 domain/markets 5 +domain/core 5 data/account 5 +domain/onramp/models 4 data/tokens 4 +domain/nft/models 3 +domain/balance-hiding 3 data/wallet-manager 3 data/manage-tokens 3 +domain/txhistory/models 2 +domain/networks 2 core/config-toggles 2 +test/mock 1 +domain/yield-supply/models 1 +domain/wallets/models 1 +domain/transaction/models 1 +domain/quotes 1 +domain/promo 1 +domain/onboarding 1 +domain/markets/models 1 +domain/feedback/models 1 +domain/express/models 1 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/account/detekt-baseline-main.xml b/domain/account/detekt-baseline-main.xml new file mode 100644 index 0000000000..10f97ccab0 --- /dev/null +++ b/domain/account/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return } + MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return@channelFlow } + NamedArguments:AddCryptoPortfolioUseCase.kt$AddCryptoPortfolioUseCase$createAccount(userWalletId, accountName, icon, derivationIndex) + UnnecessaryAbstractClass:MultiAccountListSupplier.kt$MultiAccountListSupplier$MultiAccountListSupplier + UnnecessaryAbstractClass:SingleAccountListSupplier.kt$SingleAccountListSupplier$SingleAccountListSupplier + UnnecessaryAbstractClass:SingleAccountSupplier.kt$SingleAccountSupplier$SingleAccountSupplier + + diff --git a/domain/balance-hiding/detekt-baseline-main.xml b/domain/balance-hiding/detekt-baseline-main.xml new file mode 100644 index 0000000000..6917fe3471 --- /dev/null +++ b/domain/balance-hiding/detekt-baseline-main.xml @@ -0,0 +1,9 @@ + + + + + MultilineLambdaItParameter:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest } + NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest } + NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) } + + diff --git a/domain/core/detekt-baseline-main.xml b/domain/core/detekt-baseline-main.xml new file mode 100644 index 0000000000..c27890d54a --- /dev/null +++ b/domain/core/detekt-baseline-main.xml @@ -0,0 +1,11 @@ + + + + + IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$put(key = key, value = flow) + IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$remove(key) + MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { put(key = key, value = flow) } } + MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { remove(key) } } + ObjectExtendsThrowable:DataError.kt$DataError.NetworkError$NoInternetConnection : NetworkError + + diff --git a/domain/express/models/detekt-baseline-main.xml b/domain/express/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..0c9aed8c52 --- /dev/null +++ b/domain/express/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + ObjectExtendsThrowable:ExpressError.kt$ExpressError$UnknownError : ExpressError + + diff --git a/domain/feedback/models/detekt-baseline-main.xml b/domain/feedback/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..4d318997ac --- /dev/null +++ b/domain/feedback/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + BooleanPropertyNaming:WalletMetaInfo.kt$WalletMetaInfo$val hotWalletIsBackedUp: Boolean? = null + + diff --git a/domain/markets/models/detekt-baseline-main.xml b/domain/markets/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..17228539ba --- /dev/null +++ b/domain/markets/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + BooleanPropertyNaming:TokenMarketInfo.kt$TokenMarketInfo.Network$val exchangeable: Boolean + + diff --git a/domain/models/detekt-baseline-main.xml b/domain/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c26371c864 --- /dev/null +++ b/domain/models/detekt-baseline-main.xml @@ -0,0 +1,27 @@ + + + + + BooleanPropertyNaming:ShortArticle.kt$ShortArticle$val viewed: Boolean + BooleanPropertyNaming:TokenReceiveConfig.kt$TokenReceiveConfig$val showMemoDisclaimer: Boolean + BooleanPropertyNaming:UserWallet.kt$UserWallet.Hot$val backedUp: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val signatureVerification: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddress: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddresses: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Amount$val required: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Duration$val required: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.TronResource$val required: Boolean + CastNullableToNonNullableType:DerivationPathAdapterWithMigration.kt$DerivationPathAdapterWithMigration$as + MultilineLambdaItParameter:MobileWallet.kt$MobileWallet${ ExtendedPublicKey( publicKey = publicKey, chainCode = it, ) } + NoNameShadowing:Account.kt$Account.CryptoPortfolio.Companion$derivationIndex + NullableBooleanCheck:CryptoCurrency.kt$CryptoCurrency$iconUrl?.isNotBlank() ?: true + NullableToStringCall:AccountName.kt$AccountName.Error.Empty$${Empty::class.simpleName} + NullableToStringCall:AccountName.kt$AccountName.Error.ExceedsMaxLength$${ExceedsMaxLength::class.simpleName} + NullableToStringCall:DerivationIndex.kt$DerivationIndex.Error.NegativeDerivationIndex$${this::class.simpleName} + UnsafeCallOnNullableType:MobileWalletAsStringSerializer.kt$MobileWalletAsStringSerializer$moshi.adapter(MobileWallet::class.java).fromJson(decoder.decodeString())!! + UnsafeCallOnNullableType:ScanResponseAsStringSerializer.kt$ScanResponseAsStringSerializer$moshi.adapter(ScanResponse::class.java).fromJson(decoder.decodeString())!! + UseEmptyCounterpart:ScanResponse.kt$ScanResponse$mapOf() + UseOrEmpty:CardDTO.kt$CardDTO.FirmwareVersion$type.rawValue ?: "" + UseOrEmpty:UserWalletId.kt$UserWalletId$value?.toHexString() ?: "" + + diff --git a/domain/networks/detekt-baseline-main.xml b/domain/networks/detekt-baseline-main.xml new file mode 100644 index 0000000000..eaf966bb38 --- /dev/null +++ b/domain/networks/detekt-baseline-main.xml @@ -0,0 +1,8 @@ + + + + + UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier + UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier + + diff --git a/domain/nft/models/detekt-baseline-main.xml b/domain/nft/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..526b4c8eb4 --- /dev/null +++ b/domain/nft/models/detekt-baseline-main.xml @@ -0,0 +1,9 @@ + + + + + MultilineLambdaItParameter:NFTCollections.kt${ it.content is NFTCollections.Content.Error || it.content is NFTCollections.Content.Collections && it.content.source == StatusSource.ONLY_CACHE } + MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections && content.collections.isNullOrEmpty() } + MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections && content.source != StatusSource.CACHE } + + diff --git a/domain/onboarding/detekt-baseline-main.xml b/domain/onboarding/detekt-baseline-main.xml new file mode 100644 index 0000000000..f6f53634a7 --- /dev/null +++ b/domain/onboarding/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + SuspendFunSwallowedCancellation:WasTwinsOnboardingShownUseCase.kt$WasTwinsOnboardingShownUseCase$runCatching + + diff --git a/domain/onramp/detekt-baseline-main.xml b/domain/onramp/detekt-baseline-main.xml new file mode 100644 index 0000000000..59b9829455 --- /dev/null +++ b/domain/onramp/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + MaxChainedCallsOnSameLine:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$offer.quote.paymentMethod.type.getProcessingSpeed().speed + MultilineLambdaItParameter:GetOnrampQuotesUseCase.kt$GetOnrampQuotesUseCase${ when (it) { is OnrampQuote.Data -> it.toAmount.value is OnrampQuote.Error -> null // negative difference to sort both when data and unavailable is present is OnrampQuote.AmountError -> { when (val error = it.error) { is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value } } } } + NamedArguments:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$determineAdvantages( recentOffer, bestRateOffer, fastestOffer, isSingleOffer, ) + UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PAYMENT_METHOD, paymentMethod) } + UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PROVIDER, providerName) } + UseEmptyCounterpart:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent$mapOf() + + diff --git a/domain/onramp/models/detekt-baseline-main.xml b/domain/onramp/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..b479d48536 --- /dev/null +++ b/domain/onramp/models/detekt-baseline-main.xml @@ -0,0 +1,10 @@ + + + + + BooleanPropertyNaming:OnrampCountry.kt$OnrampCountry$val onrampAvailable: Boolean + ObjectExtendsThrowable:OnrampPairsError.kt$OnrampPairsError$PairsNotFound : OnrampPairsError + ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$VerificationFailed : OnrampRedirectError + ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$WrongRequestId : OnrampRedirectError + + diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 8bc240224a..11049ae4d2 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -96,9 +96,9 @@ class GetOnrampOffersUseCase( isMoonpayPromoActive: Boolean, ): OnrampOffer? { val moonpayPromoOffers = if (isMoonpayPromoActive) { - offers.filter { - it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && - it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + offers.filter { offer -> + offer.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + offer.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY } } else { emptyList() diff --git a/domain/promo/detekt-baseline-main.xml b/domain/promo/detekt-baseline-main.xml new file mode 100644 index 0000000000..b913db3534 --- /dev/null +++ b/domain/promo/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableBooleanCheck:GetStoryContentUseCase.kt$GetStoryContentUseCase$isFCAAllowed(id).firstOrNull() ?: false + + diff --git a/domain/quotes/detekt-baseline-main.xml b/domain/quotes/detekt-baseline-main.xml new file mode 100644 index 0000000000..bb2982d875 --- /dev/null +++ b/domain/quotes/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + UnnecessaryAbstractClass:SingleQuoteStatusSupplier.kt$SingleQuoteStatusSupplier$SingleQuoteStatusSupplier + + diff --git a/domain/staking/models/detekt-baseline-main.xml b/domain/staking/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..865d3ce960 --- /dev/null +++ b/domain/staking/models/detekt-baseline-main.xml @@ -0,0 +1,19 @@ + + + + + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Metadata.Fee$val enabled: Boolean + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val enter: Boolean + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val exit: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Enter$val skipEnterAmount: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Exit$val partiallyUnstakeDisabled: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Pending.Stake$val skipEnterAmount: Boolean + BooleanPropertyNaming:Yield.kt$AddressArgument$val required: Boolean + BooleanPropertyNaming:Yield.kt$Yield$val allValidatorsFull: Boolean = validators.all { it.status == Validator.ValidatorStatus.FULL } + BooleanPropertyNaming:Yield.kt$Yield.Metadata$val supportsMultipleValidators: Boolean? + BooleanPropertyNaming:Yield.kt$Yield.Metadata.Enabled$val enabled: Boolean + BooleanPropertyNaming:Yield.kt$Yield.Status$val enter: Boolean + BooleanPropertyNaming:Yield.kt$Yield.Status$val exit: Boolean? + BooleanPropertyNaming:Yield.kt$Yield.Validator$val preferred: Boolean + + diff --git a/domain/tokens/models/detekt-baseline-main.xml b/domain/tokens/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..80759d1dfb --- /dev/null +++ b/domain/tokens/models/detekt-baseline-main.xml @@ -0,0 +1,13 @@ + + + + + ObjectExtendsThrowable:RemoveCurrencyError.kt$RemoveCurrencyError$HasLinkedTokens : RemoveCurrencyError + UseEmptyCounterpart:PromoAnalyticsEvent.kt$PromoAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenExchangeAnalyticsEvent.kt$TokenExchangeAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenOnrampAnalyticsEvent.kt$TokenOnrampAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenReceiveAnalyticsEvent.kt$TokenReceiveAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenReceiveNewAnalyticsEvent.kt$TokenReceiveNewAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenScreenAnalyticsEvent.kt$TokenScreenAnalyticsEvent$mapOf() + + diff --git a/domain/transaction/models/detekt-baseline-main.xml b/domain/transaction/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c826f4a41a --- /dev/null +++ b/domain/transaction/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableToStringCall:SendTransactionError.kt$SendTransactionError$$code + + diff --git a/domain/txhistory/models/detekt-baseline-main.xml b/domain/txhistory/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c1da5a5bd3 --- /dev/null +++ b/domain/txhistory/models/detekt-baseline-main.xml @@ -0,0 +1,8 @@ + + + + + ObjectExtendsThrowable:TxHistoryStateError.kt$TxHistoryStateError$EmptyTxHistories : TxHistoryStateError + ObjectExtendsThrowable:TxHistoryStateError.kt$TxHistoryStateError$TxHistoryNotImplemented : TxHistoryStateError + + diff --git a/domain/visa/models/detekt-baseline-main.xml b/domain/visa/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..674a5f7aed --- /dev/null +++ b/domain/visa/models/detekt-baseline-main.xml @@ -0,0 +1,14 @@ + + + + + NamedArguments:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$ActivationStarted( value.activationInput!!, value.authTokens!!, value.remoteState!!, value.cardWalletAddress!!, ) + NamedArguments:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$VisaCardActivationStatus_Json( VisaCardActivationStatus_Type.ActivationStarted, value.activationInput, value.authTokens, value.remoteState, value.cardWalletAddress, ) + UnsafeCallOnNullableType:VisaActivationRemoteState.kt$VisaActivationRemoteState_JsonAdapter$value.activationOrderInfo!! + UnsafeCallOnNullableType:VisaActivationRemoteState.kt$VisaActivationRemoteState_JsonAdapter$value.awaitingPinCodeStatus!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.activationInput!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.authTokens!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.cardWalletAddress!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.remoteState!! + + diff --git a/domain/wallet-connect/models/detekt-baseline-main.xml b/domain/wallet-connect/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..a019799a9b --- /dev/null +++ b/domain/wallet-connect/models/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + BooleanPropertyNaming:WcAppMetaData.kt$WcAppMetaData$val linkMode: Boolean = false + BooleanPropertyNaming:WcSession.kt$WcSession$val showWalletInfo: Boolean + ObjectExtendsThrowable:WcPairError.kt$WcPairError$InvalidConnectionRequest : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$InvalidDomainURL : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$ProposalExpired : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$RejectionFailed : WcPairError + + diff --git a/domain/wallet-manager/models/detekt-baseline-main.xml b/domain/wallet-manager/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..ecf2e0cce8 --- /dev/null +++ b/domain/wallet-manager/models/detekt-baseline-main.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/domain/wallets/models/detekt-baseline-main.xml b/domain/wallets/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..0b3473e462 --- /dev/null +++ b/domain/wallets/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + ObjectExtendsThrowable:ParsedQrCodeErrors.kt$ParsedQrCodeErrors$InvalidUriError : ParsedQrCodeErrors + + diff --git a/domain/yield-supply/models/detekt-baseline-main.xml b/domain/yield-supply/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..9407c35741 --- /dev/null +++ b/domain/yield-supply/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableToStringCall:YieldMarketToken.kt$YieldMarketToken$${backendId} + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile index b62893f38d..f1162f841b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :android do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") - gradle(task: "detekt detektGoogleDebug detektDebug --continue") + gradle(task: "detekt detektMain") end desc "Run tests" diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt index 264610cc2a..c6778122da 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt @@ -5,6 +5,7 @@ import com.tangem.plugin.configuration.utils.findPlugin import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT_PLUGINS import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -28,6 +29,9 @@ private fun DetektExtension.configure(project: Project) { "plugins/detekt-rules/app-detekt-config.yml" ) ) + + ignoredBuildTypes = listOf("release", "internal", "external", "mocked") + ignoredFlavors = listOf("huawei") } private fun Project.configureDetektPlugins() { @@ -41,9 +45,14 @@ private fun Project.configureDetektPlugins() { } private fun Project.configureDetektTask() { - tasks.withType { - include("**/*.kt") - exclude("**/resources/**", "**/build/**") + tasks.withType().configureEach { + logger.lifecycle("[Detekt] Configuring task: $name in project: ${project.path}") + + source = fileTree(projectDir) { + include("src/main/**/*.kt") + exclude("**/build/**") + } + reports { sarif { required.set(false) @@ -55,4 +64,14 @@ private fun Project.configureDetektTask() { jvmTarget = "17" } + + tasks.withType().configureEach { + logger.lifecycle("[Detekt Baseline] Configuring task: $name in project: ${project.path}") + + source = fileTree(projectDir) { + include("src/main/**/*.kt") + exclude("**/build/**") + } + jvmTarget = "17" + } } \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index 3da4c865da..d7950d60bb 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 3da4c865da5f2a61d5170357c60de4ddd8425815 +Subproject commit d7950d60bb4c6353f2aa3364073c6ec72167c666 diff --git a/test/mock/detekt-baseline-main.xml b/test/mock/detekt-baseline-main.xml new file mode 100644 index 0000000000..dd89921096 --- /dev/null +++ b/test/mock/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + MultilineLambdaItParameter:MockAccounts.kt$MockAccounts${ val account = createAccount(derivationIndex = it + 1, userWalletId = userWalletId) add(account) } + + diff --git a/update_detekt_baseline.sh b/update_detekt_baseline.sh index aa13af5586..b30b5d31f0 100755 --- a/update_detekt_baseline.sh +++ b/update_detekt_baseline.sh @@ -7,7 +7,7 @@ cd "$SCRIPT_DIR" OUTPUT_FILE="${1:-detekt_baseline_report.txt}" -INITIAL_ISSUES=1802 +INITIAL_ISSUES=1933 log() { echo "$@" | tee -a "$OUTPUT_FILE" @@ -24,7 +24,7 @@ log "" log "Step 1: Running detekt to check for new issues..." log "" -if ./gradlew detekt detektDebug detektGoogleDebug; then +if ./gradlew detekt detektMain; then log "✓ Detekt passed - no new issues found" log "" else @@ -35,10 +35,10 @@ else exit 1 fi -log "Step 2: Updating detekt baseline for debug variant..." +log "Step 2: Updating detekt baseline ..." log "" -./gradlew detektBaselineDebug +./gradlew detektBaselineMain log "" log "Baseline updated successfully!" @@ -54,11 +54,11 @@ module_count=0 temp_file=$(mktemp) -find . -name "detekt-baseline-debug.xml" -type f | while IFS= read -r file; do +find . \( -name "detekt-baseline-main.xml" -o -name "detekt-baseline-debug.xml" \) -type f | while IFS= read -r file; do 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$||') + module_name=$(echo "$file" | sed 's|^\./||' | sed 's|/detekt-baseline-main.xml$||' | sed 's|/detekt-baseline-debug.xml$||') echo "$issue_count|$module_name" fi done | sort -rn -t'|' -k1 > "$temp_file" From 370690a3598d31451e041a67c0f6bb9c378f0d05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 14:59:31 +0700 Subject: [PATCH 066/130] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../models/event/MainScreenAnalyticsEvent.kt | 8 ++ .../pair/DefaultWcPairUseCase.kt | 1 - .../analytics/TokenScreenAnalyticsEvent.kt | 9 ++ .../domain/walletconnect/WcAnalyticEvents.kt | 21 +++- .../analytics/WalletSettingsAnalyticEvents.kt | 23 +++- .../account/PortfolioSelectorComponent.kt | 1 + .../AccountSettingsAnalyticEvents.kt | 100 ++++++++++++++++++ .../WalletSettingsAccountAnalyticEvents.kt | 26 +++++ .../archived/ArchivedAccountListModel.kt | 6 ++ .../createedit/AccountCreateEditModel.kt | 33 ++++++ .../account/details/AccountDetailsModel.kt | 18 +++- .../DefaultPortfolioSelectorController.kt | 2 + .../analytics/CustomTokenAnalyticsEvent.kt | 13 +++ .../component/CustomTokenFormComponent.kt | 3 +- .../impl/DefaultAddCustomTokenComponent.kt | 12 ++- .../model/CustomTokenFormModel.kt | 2 +- .../add/impl/model/AddToPortfolioModel.kt | 12 +++ .../portfolio/add/impl/model/AddTokenModel.kt | 11 +- .../add/impl/model/TokenActionsModel.kt | 5 +- .../add/impl/model/TokenActionsUiBuilder.kt | 7 +- .../impl/analytics/PortfolioAnalyticsEvent.kt | 22 ++++ .../impl/model/MarketsPortfolioModel.kt | 5 +- .../referral/analytics/ReferralEvents.kt | 8 +- .../feature/referral/model/ReferralModel.kt | 2 + .../api/analytics/CommonSendAnalyticEvents.kt | 19 +++- .../send/v2/send/DefaultSendComponent.kt | 7 ++ .../v2/send/analytics/SendAnalyticEvents.kt | 20 ++-- .../v2/send/analytics/SendAnalyticHelper.kt | 6 +- .../v2/send/confirm/model/SendConfirmModel.kt | 6 +- .../v2/sendnft/DefaultNFTSendComponent.kt | 7 ++ .../DefaultSendWithSwapComponent.kt | 7 ++ .../feature/swap/analytics/SwapEvents.kt | 4 + .../tangem/feature/swap/model/SwapModel.kt | 4 + .../tokendetails/model/TokenDetailsModel.kt | 10 ++ .../model/WalletSettingsModel.kt | 17 ++- .../utils/AccountItemsDelegate.kt | 20 +++- .../utils/AccountListSortingSaver.kt | 6 ++ .../wallet/child/wallet/model/WalletModel.kt | 17 ++- .../intents/WalletContentClickIntents.kt | 2 + .../analytics/WalletScreenAnalyticsEvent.kt | 6 +- .../connections/model/WcPairModel.kt | 12 +++ 42 files changed, 486 insertions(+), 35 deletions(-) create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt 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 8ea55f5139..9488b9c840 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 @@ -266,5 +266,6 @@ sealed class AnalyticsParam { const val CHOSEN_TOKEN = "Token Chosen" const val ENS = "ENS" const val ENS_ADDRESS = "ENS Address" + const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)" } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index a1a2ef6911..b511dd504e 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -56,6 +56,14 @@ sealed class MainScreenAnalyticsEvent( event = "Button - Explore", ) + class AccountShowTokens : MainScreenAnalyticsEvent( + event = "Button - Account Show Tokens", + ) + + class AccountHideTokens : MainScreenAnalyticsEvent( + event = "Button - Account Hide Tokens", + ) + data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( event = "Button - Swap", params = mapOf(AnalyticsParam.STATUS to status.value), diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 6e6396e89a..f07bf2bd9d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -166,7 +166,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } override fun approve(sessionForApprove: WcSessionApprove) { - analytics.send(WcAnalyticEvents.PairButtonConnect()) onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 5c0c30da0d..41a0965407 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION import com.tangem.core.analytics.models.AnalyticsParam.Key.BALANCE import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN @@ -58,12 +59,14 @@ sealed class TokenScreenAnalyticsEvent( token: String, blockchain: String, status: String?, + derivationIndex: Int? = null, ) : TokenScreenAnalyticsEvent( event = event, params = buildMap { put(TOKEN_PARAM, token) put(BLOCKCHAIN, blockchain) status?.let { put(STATUS, it) } + derivationIndex?.let { put(ACCOUNT_DERIVATION_FROM, it.toString()) } }, ) { @@ -71,11 +74,13 @@ sealed class TokenScreenAnalyticsEvent( token: String, blockchain: String, status: String?, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Buy", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonSell( @@ -93,22 +98,26 @@ sealed class TokenScreenAnalyticsEvent( token: String, status: String, blockchain: String, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Exchange", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonSend( token: String, status: String, blockchain: String, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Send", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonReceive( diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 6a0ac90cb8..aba74660de 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -277,4 +277,23 @@ fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { SAFE -> DAppVerificationStatus.Verified UNSAFE -> DAppVerificationStatus.Risky FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown -}.status \ No newline at end of file +}.status + +sealed class WcAnalyticAccountEvents( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = WC_CATEGORY_ACCOUNT_NAME, event = event, params = params) { + + data class PairButtonConnect( + private val accountDerivation: Int, + ) : WcAnalyticAccountEvents( + event = "Button - Connect", + params = mapOf( + "Account Derivation" to accountDerivation.toString(), + ), + ) + + companion object { + const val WC_CATEGORY_ACCOUNT_NAME = "WalletConnect - Account" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 93d7d9b6e1..1c23c85475 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -16,14 +16,35 @@ sealed class WalletSettingsAnalyticEvents( params = mapOf(STATUS to enabled.value), ) - class WalletSettingsScreenOpened : WalletSettingsAnalyticEvents( + class WalletSettingsScreenOpened( + private val accountsCount: Int?, + ) : WalletSettingsAnalyticEvents( event = "Wallet Settings Screen Opened", + params = buildMap { + if (accountsCount != null) put("Accounts Count", accountsCount.toString()) + }, ) class ButtonBackup : WalletSettingsAnalyticEvents( event = "Button - Backup", ) + class ButtonAddAccount : WalletSettingsAnalyticEvents( + event = "Button - Add Account", + ) + + class ButtonOpenExistingAccount : WalletSettingsAnalyticEvents( + event = "Button - Open Existing Account", + ) + + class ButtonArchivedAccounts : WalletSettingsAnalyticEvents( + event = "Button - Archived Accounts", + ) + + class LongtapAccountsOrder : WalletSettingsAnalyticEvents( + event = "Longtap - Accounts Order", + ) + data class ButtonAccessCode( private val isCodeSet: Boolean, ) : WalletSettingsAnalyticEvents( diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt index 0b98d6b218..50c01a8d05 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt @@ -57,6 +57,7 @@ interface PortfolioSelectorController { * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> + suspend fun isAccountModeSync(): Boolean fun selectAccount(accountId: AccountId?) fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt new file mode 100644 index 0000000000..dc8afe9786 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt @@ -0,0 +1,100 @@ +package com.tangem.features.account.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.AccountCreateEditComponent + +sealed class AccountSettingsAnalyticEvents( + category: String = "Settings / Account", + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category, event, params) { + + class AccountSettingsScreenOpened : AccountSettingsAnalyticEvents( + event = "Account Settings Screen Opened", + ) + + class ButtonManageTokens : AccountSettingsAnalyticEvents( + event = "Button - Manage Tokens", + ) + + class ButtonArchiveAccount : AccountSettingsAnalyticEvents( + event = "Button - Archive Account", + ) + + class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents( + event = "Button - Archive Account Confirmation", + ) + + class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents( + event = "Button - Cancel Account Archivation", + ) + + class AccountArchived : AccountSettingsAnalyticEvents( + event = "Account Archived", + ) + + class ButtonEdit : AccountSettingsAnalyticEvents( + event = "Button - Edit", + ) + + class AccountEditScreenOpened : AccountSettingsAnalyticEvents( + event = "Account Edit Screen Opened", + ) + + class ButtonSave( + val name: AccountName, + val icon: CryptoPortfolioIcon, + ) : AccountSettingsAnalyticEvents( + event = "Button - Save", + params = buildMap { + val accountName = when (name) { + is AccountName.Custom -> name.value + AccountName.DefaultMain -> "DefaultMain" + } + put("Name", accountName) + put("Color", icon.color.name) + put("Icon", icon.value.name) + }, + ) + + class ButtonAddNewAccount( + val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: Int, + ) : AccountSettingsAnalyticEvents( + event = "Button - Add New Account", + params = buildMap { + val accountName = when (name) { + is AccountName.Custom -> name.value + AccountName.DefaultMain -> "DefaultMain" + } + put("Name", accountName) + put("Color", icon.color.name) + put("Icon", icon.value.name) + put("Derivation", derivationIndex.toString()) + }, + ) + + class AccountError( + val source: Source, + val error: String, + ) : AccountSettingsAnalyticEvents( + event = "Account Error", + params = buildMap { + put("Error", error) + }, + ) + + enum class Source(val value: String) { + NEW_ACCOUNT("New Account"), EDIT("Edit"), ARCHIVE("Archive") + } + + companion object { + fun AccountCreateEditComponent.Params.toAnalyticSource() = when (this) { + is AccountCreateEditComponent.Params.Create -> Source.NEW_ACCOUNT + is AccountCreateEditComponent.Params.Edit -> Source.EDIT + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt new file mode 100644 index 0000000000..165fe01945 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt @@ -0,0 +1,26 @@ +package com.tangem.features.account.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class WalletSettingsAccountAnalyticEvents( + category: String = "Settings / Wallet Settings", + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category, event, params) { + + class AccountCreated : WalletSettingsAccountAnalyticEvents( + event = "Account Created", + ) + + class AccountRecovered : WalletSettingsAccountAnalyticEvents( + event = "Account Recovered", + ) + + class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents( + event = "Archived Accounts Screen Opened", + ) + + class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents( + event = "Button - Recover Account", + ) +} \ No newline at end of file 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 c515c6eafd..13a3acdaf9 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 @@ -1,5 +1,6 @@ package com.tangem.features.account.archived +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.model.Model @@ -18,6 +19,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase import com.tangem.domain.models.account.AccountId import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents import com.tangem.features.account.archived.entity.AccountArchivedUM import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress @@ -40,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor( private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase, private val umBuilder: AccountArchivedUMBuilder, + private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { @@ -52,6 +55,7 @@ internal class ArchivedAccountListModel @Inject constructor( private val getArchivedAccountsJob = JobHolder() init { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened()) getArchivedAccounts() } @@ -96,6 +100,7 @@ internal class ArchivedAccountListModel @Inject constructor( } private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount()) uiState.update { it.toggleProgress(accountId, isLoading = true) } val result = withContext(dispatchers.default) { recoverCryptoPortfolioUseCase(accountId) @@ -148,6 +153,7 @@ internal class ArchivedAccountListModel @Inject constructor( } private fun showSuccessRecoverMessage() { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountRecovered()) val message = resourceReference(R.string.account_recover_success_message) messageSender.send(ToastMessage(message = message)) } 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 6387e99fc8..8720ae4650 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 @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.account.toDomain import com.tangem.common.ui.account.toUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped @@ -25,6 +26,9 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Companion.toAnalyticSource +import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon @@ -54,6 +58,7 @@ internal class AccountCreateEditModel @Inject constructor( private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -65,6 +70,8 @@ internal class AccountCreateEditModel @Inject constructor( init { if (params is AccountCreateEditComponent.Params.Create) { updateDerivationInfo(userWalletId = params.userWalletId) + } else { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened()) } } @@ -105,6 +112,12 @@ internal class AccountCreateEditModel @Inject constructor( val icon = state.account.portfolioIcon.toDomain() val index = state.account.derivationInfo.index ?: return val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount( + name = name, + icon = icon, + derivationIndex = derivationIndex.value, + ) + analyticsEventHandler.send(event) uiState.value = uiState.value.toggleProgress(showProgress = true) val result = addCryptoPortfolioUseCase( @@ -118,12 +131,22 @@ internal class AccountCreateEditModel @Inject constructor( result .onLeft(::handleAddAccountError) .onRight { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated()) showMessage(R.string.account_create_success_message) router.pop() } } private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) { + val event = AccountSettingsAnalyticEvents.AccountError( + source = params.toAnalyticSource(), + error = when (error) { + is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag + is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() + }, + ) + analyticsEventHandler.send(event) + val isDuplicateAccountNamesError = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames when { @@ -141,6 +164,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = state.account.portfolioIcon.toDomain() val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon)) uiState.value = uiState.value.toggleProgress(showProgress = true) val result = updateCryptoPortfolioUseCase( @@ -159,6 +183,15 @@ internal class AccountCreateEditModel @Inject constructor( } private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) { + val event = AccountSettingsAnalyticEvents.AccountError( + source = params.toAnalyticSource(), + error = when (error) { + is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag + is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() + UpdateCryptoPortfolioUseCase.Error.NothingToUpdate -> error::class.simpleName.orEmpty() + }, + ) + analyticsEventHandler.send(event) val isDuplicateAccountNamesError = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames when { 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 4be0bb85f1..c1bdf53342 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 @@ -2,6 +2,7 @@ package com.tangem.features.account.details import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.toUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,6 +21,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.details.entity.AccountDetailsUM import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode @@ -37,6 +39,7 @@ internal class AccountDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase, singleAccountSupplier: SingleAccountSupplier, + private val analyticsEventHandler: AnalyticsEventHandler, private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { @@ -48,12 +51,14 @@ internal class AccountDetailsModel @Inject constructor( private val accountId = params.account.accountId init { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened()) singleAccountSupplier(SingleAccountProducer.Params(accountId)) .onEach { account -> uiState.update { buildUI(account) } } .launchIn(modelScope) } private fun onEditAccountClick(account: Account) { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit()) router.push(AppRoute.EditAccount(account)) } @@ -62,17 +67,21 @@ internal class AccountDetailsModel @Inject constructor( source = AppRoute.ManageTokens.Source.SETTINGS, portfolioId = PortfolioId(account.accountId), ) + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens()) router.push(route) } private fun onArchiveAccountClick() { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount()) confirmArchiveDialog() } private fun confirmArchiveDialog() { val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), - onClick = {}, + onClick = { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation()) + }, ) val firstAction = EventMessageAction( title = resourceReference(R.string.account_details_archive_action), @@ -90,6 +99,7 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation()) uiState.update { it.toggleProgress(true) } archiveCryptoPortfolioUseCase(accountId) .onLeft { error -> @@ -97,6 +107,7 @@ internal class AccountDetailsModel @Inject constructor( uiState.update { it.toggleProgress(false) } } .onRight { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountArchived()) val message = resourceReference(R.string.account_archive_success_message) messageSender.send(ToastMessage(message = message)) router.pop() @@ -104,6 +115,11 @@ internal class AccountDetailsModel @Inject constructor( } private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) { + val event = AccountSettingsAnalyticEvents.AccountError( + source = AccountSettingsAnalyticEvents.Source.ARCHIVE, + error = error.tag, + ) + analyticsEventHandler.send(event) val titleRes = R.string.common_something_went_wrong val messageRes = when (error) { is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index 00b2a52f71..fe9a055c3f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -29,6 +29,8 @@ internal class DefaultPortfolioSelectorController @Inject constructor( override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true } + override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync() + override fun selectAccount(accountId: AccountId?) { _selectedAccount.tryEmit(accountId) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt index a2c559ac76..c3a41d047c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt @@ -31,6 +31,19 @@ internal sealed class CustomTokenAnalyticsEvent( ), ) + class AddTokenToAnotherAccount( + currencySymbol: String, + derivationPath: String, + source: ManageTokensSource, + ) : CustomTokenAnalyticsEvent( + event = "Button - Add Token To Another Account", + params = mapOf( + AnalyticsParam.Key.TOKEN_PARAM to currencySymbol, + AnalyticsParam.Key.DERIVATION to derivationPath, + AnalyticsParam.Key.SOURCE to source.analyticsName, + ), + ) + class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent( event = "Custom Token Network Selected", params = mapOf( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 2baf3f982f..3fd1eb6250 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork @@ -16,7 +17,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val source: ManageTokensSource, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, - val onCurrencyAdded: () -> Unit, + val onCurrencyAdded: (currency: CryptoCurrency) -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 8e270519a8..b49478560f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.AddCustomTokenComponent @@ -232,7 +233,16 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( } } - private fun dismissAndNotify() { + private fun dismissAndNotify(currency: CryptoCurrency) { + val account = addedToAccount + if (account is Account.CryptoPortfolio && !account.isMainAccount) { + val event = CustomTokenAnalyticsEvent.AddTokenToAnotherAccount( + currencySymbol = currency.symbol, + derivationPath = currency.network.derivationPath.value.orEmpty(), + source = params.source, + ) + analyticsEventHandler.send(event) + } params.onCurrencyAdded(addedToAccount) dismiss() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 60b26282fb..ff7a6c3c36 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -364,7 +364,7 @@ internal class CustomTokenFormModel @Inject constructor( return@resource } - params.onCurrencyAdded() + params.onCurrencyAdded(currency) } private fun selectNetwork() { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index f165c1bb40..40c339c730 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.popToFirst import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -47,6 +48,7 @@ internal class AddToPortfolioModel @Inject constructor( private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, private val messageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, val portfolioSelectorController: PortfolioSelectorController, ) : Model(), ChooseNetworkComponent.Callbacks by callbackDelegate, @@ -93,6 +95,7 @@ internal class AddToPortfolioModel @Inject constructor( .map { it.availableToAddData } .distinctUntilChanged() .stateIn(this) + val isAccountMode = portfolioSelectorController.isAccountModeSync() // use snapshot data, looks like we don’t need to remap at runtime val data = featureDataFlow.value @@ -117,6 +120,7 @@ internal class AddToPortfolioModel @Inject constructor( // force select a portfolio, triggers [selectedPortfolio] portfolioSelectorController.selectAccount(accountId) } else { + logAccountSelector(isAccountMode) navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector) } @@ -165,6 +169,7 @@ internal class AddToPortfolioModel @Inject constructor( .onEach { middleNavigationJob?.cancel() middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) + logAccountSelector(isAccountMode) navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) } .launchIn(this) @@ -194,6 +199,12 @@ internal class AddToPortfolioModel @Inject constructor( .launchIn(modelScope) } + private fun logAccountSelector(isAccountMode: Boolean) { + if (isAccountMode) { + analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) + } + } + private fun changeNetworkNavigationFlow(): Flow { return setupNetworkFlow(selectedPortfolio) .onEach { newNetwork -> @@ -260,6 +271,7 @@ internal class AddToPortfolioModel @Inject constructor( data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null val availableToAddAccount = availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null + if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged()) SelectedPortfolio( isAccountMode = isAccountMode, userWallet = availableToAddWallets.userWallet, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 8a4ec65436..b0f322b6f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.add.impl.model +import com.tangem.common.ui.addtoken.AddTokenUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -10,13 +11,13 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.models.account.Account import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.common.ui.addtoken.AddTokenUM import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -74,7 +75,8 @@ internal class AddTokenModel @Inject constructor( analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) val cryptoCurrency = selectedNetwork.cryptoCurrency - val accountId = selectedPortfolio.account.account.account.accountId + val account = selectedPortfolio.account.account.account + val accountId = account.accountId manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { processError(error = it) @@ -90,6 +92,11 @@ internal class AddTokenModel @Inject constructor( if (status == null) { processError(error = null) } else { + when (account) { + is Account.CryptoPortfolio -> if (!account.isMainAccount) { + analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount()) + } + } params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt index a2d0ceeba2..d3dd8d929b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt @@ -62,10 +62,7 @@ internal class TokenActionsModel @Inject constructor( ) private fun handledQuickAction(handledAction: HandledQuickAction) { - val event = analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, - ) + val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action) analyticsEventHandler.send(event) val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive if (!isReceive) return diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt index be2f06ccdf..6a1dcb993e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.add.impl.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -15,6 +16,7 @@ import javax.inject.Inject @ModelScoped internal class TokenActionsUiBuilder @Inject constructor( paramsContainer: ParamsContainer, + private val analyticsEventHandler: AnalyticsEventHandler, ) { private val params = paramsContainer.require() @@ -32,7 +34,10 @@ internal class TokenActionsUiBuilder @Inject constructor( ) return TokenActionsUM( token = tokenUM, - onLaterClick = { params.callbacks.onLaterClick() }, + onLaterClick = { + analyticsEventHandler.send(params.eventBuilder.getTokenLater()) + params.callbacks.onLaterClick() + }, quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index 9e02ec98a2..772d83f651 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -21,6 +21,14 @@ internal class PortfolioAnalyticsEvent( ), ) + fun popupToChooseAccount() = PortfolioAnalyticsEvent( + event = "Popup to choose account", + ) + + fun addToNotMainAccount() = PortfolioAnalyticsEvent( + event = "Button - Add (token not to main Account)", + ) + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( @@ -47,5 +55,19 @@ internal class PortfolioAnalyticsEvent( put("blockchain", blockchainName) }, ) + + fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent( + event = when (actionUM) { + TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy" + TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive" + TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange" + TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" + else -> "error" + }, + ) + + fun getTokenLater() = PortfolioAnalyticsEvent( + event = "Popup Get token - Button Later", + ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index d5e434ad2a..a314e9a0e1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -193,7 +193,10 @@ internal class MarketsPortfolioModel @Inject constructor( NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable } }, - onAddClick = { bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) }, + onAddClick = { + analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) + bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) + }, ) newMarketsPortfolioDelegate.combineData() .onEach { _state.value = it } diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index f92ffd3549..364b76ce0f 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -12,4 +12,10 @@ sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CAT class ParticipateSuccessful : ReferralEvents(event = "Participate Successful") } -private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" \ No newline at end of file +sealed class ReferralEventsAccounts(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_ACCOUNT_CATEGORY, event) { + + class ListChooseAccount : ReferralEventsAccounts(event = "List - choose account") +} + +private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" +private const val REFERRAL_PROGRAM_ACCOUNT_CATEGORY = "Referral program - Account" \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index ac7b852a3a..46ce302791 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -30,6 +30,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.analytics.ReferralEvents +import com.tangem.feature.referral.analytics.ReferralEventsAccounts import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.errors.ReferralError @@ -116,6 +117,7 @@ internal class ReferralModel @Inject constructor( private fun combineAccountUI(referralData: ReferralData): Flow = combine( flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher) + .onEach { analyticsEventHandler.send(ReferralEventsAccounts.ListChooseAccount()) } .onEach { bottomSheetNavigation.dismiss() }, flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(), flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(), diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index cb58f30134..82527c66bf 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -51,16 +51,29 @@ sealed class CommonSendAnalyticEvents( ), ) + @Suppress("NullableToStringCall") /** Confirmation screen opened */ data class ConfirmationScreenOpened( val categoryName: String, val source: CommonSendSource, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, + val sendBlockchain: String, + val sendToken: String, ) : CommonSendAnalyticEvents( category = categoryName, event = "Confirm Screen Opened", - params = mapOf( - SOURCE to source.analyticsName, - ), + params = buildMap { + put(SOURCE, source.analyticsName) + put("Token", sendToken) + put("Blockchain", sendBlockchain) + if (fromDerivationIndex != null || toDerivationIndex != null) { + put( + "Account Derivation From or To (optional)", + "$fromDerivationIndex, $toDerivationIndex", + ) + } + }, ) /** If transaction delays notification is present */ diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 858747f091..d6e89cd52c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -88,10 +88,17 @@ internal class DefaultSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SendConfirmComponent -> { + val fromCurrency = params.currency + val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value + .takeIf { model.isAccountModeFlow.value } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, source = model.analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRoute.value.isEditMode) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index bf26de5704..d8ccef5d07 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE @@ -25,18 +26,21 @@ internal sealed class SendAnalyticEvents( val blockchain: String, val isNonceNotEmpty: Boolean, private val ensStatus: AnalyticsParam.EmptyFull, + val derivationIndex: Int?, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", - params = mapOf( - TOKEN_PARAM to token, - FEE_TYPE to feeType.value, - BLOCKCHAIN to blockchain, - NONCE to isNonceNotEmpty.toString().capitalize(), - ENS_ADDRESS to when (ensStatus) { + params = buildMap { + put(TOKEN_PARAM, token) + put(FEE_TYPE, feeType.value) + put(BLOCKCHAIN, blockchain) + if (derivationIndex != null) put(ACCOUNT_DERIVATION_FROM, derivationIndex.toString()) + put(NONCE, isNonceNotEmpty.toString().capitalize()) + val ensAddress = when (ensStatus) { AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() AnalyticsParam.EmptyFull.Full -> true.toString().capitalize() - }, - ), + } + put(ENS_ADDRESS, ensAddress) + }, ) data class ConvertTokenButtonClicked( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 4d56a5075f..e65f75d699 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -4,6 +4,7 @@ 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.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -17,10 +18,12 @@ internal class SendAnalyticHelper @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) { - fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) { + fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM, account: Account.CryptoPortfolio?) { val destinationUM = sendUM.destinationUM as? DestinationUM.Content val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() + val isNotMainAccount = account != null && !account.isMainAccount + val derivationIndex = if (isNotMainAccount) account.derivationIndex.value else null analyticsEventHandler.send( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, @@ -28,6 +31,7 @@ internal class SendAnalyticHelper @Inject constructor( blockchain = cryptoCurrency.network.name, isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), + derivationIndex = derivationIndex, ), ) analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index d36c18e827..6fcd04be67 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -416,7 +416,11 @@ internal class SendConfirmModel @Inject constructor( updateTransactionStatus(txData) addTokenToWalletIfNeeded() sendBalanceUpdater.scheduleUpdates() - sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + sendAnalyticHelper.sendSuccessAnalytics( + cryptoCurrency = cryptoCurrency, + sendUM = uiState.value, + account = params.accountFlow.value, + ) params.callback.onResult(uiState.value) params.onSendTransaction() }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index c8992e8ca4..250a2adb28 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -79,10 +79,17 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is NFTSendConfirmComponent -> { + val fromCurrency = model.cryptoCurrency + val fromDerivationIndex = model.account?.derivationIndex?.value + .takeIf { model.isAccountsMode } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = analyticsCategoryName, source = analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRouteFlow.value.isEditMode) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 2006967f4f..300dd949eb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -109,10 +109,17 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> { + val fromCurrency = params.currency + val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value + .takeIf { model.isAccountModeFlow.value } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, source = model.analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRoute.value.isEditMode) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index f9f65022bc..c1ee5ff99d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -62,6 +62,7 @@ sealed class SwapEvents( class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + @Suppress("NullableToStringCall") data class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast @@ -69,6 +70,8 @@ sealed class SwapEvents( val receiveBlockchain: String, val sendToken: String, val receiveToken: String, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, ) : SwapEvents( event = "Swap in Progress Screen Opened", params = mapOf( @@ -78,6 +81,7 @@ sealed class SwapEvents( "Receive Token" to receiveToken, "Send Blockchain" to sendBlockchain, "Receive Blockchain" to receiveBlockchain, + "Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex", ), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index b9ed696598..15900bd318 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -847,6 +847,8 @@ internal class SwapModel @Inject constructor( val fee = dataState.selectedFee?.feeType ?: return val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return val toCurrency = dataState.toCryptoCurrency?.currency ?: return + val fromDerivationIndex = dataState.fromAccount?.derivationIndex?.value + val toDerivationIndex = dataState.toAccount?.derivationIndex?.value analyticsEventHandler.send( SwapEvents.SwapInProgressScreen( @@ -856,6 +858,8 @@ internal class SwapModel @Inject constructor( receiveBlockchain = toCurrency.network.name, sendToken = fromCurrency.symbol, receiveToken = toCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 1ec6d138b7..2cbe54658a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -532,6 +532,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -557,6 +558,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = null, + derivationIndex = getAccountIndexOrNull(), ), ) router.openTokenDetails(userWalletId = userWalletId, currency = cryptoCurrency) @@ -578,6 +580,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -698,6 +701,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -766,6 +770,12 @@ internal class TokenDetailsModel @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + private fun getAccountIndexOrNull(): Int? { + val account = account + val isNotMainAccount = account != null && !account.isMainAccount + return if (isNotMainAccount) account.derivationIndex.value else null + } + private fun openExplorer() { val currencyStatus = cryptoCurrencyStatus ?: return 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 6d95c6fa82..b5ba400298 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 @@ -25,6 +25,8 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.PortfolioId @@ -88,6 +90,8 @@ internal class WalletSettingsModel @Inject constructor( private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, ) : Model() { @@ -115,7 +119,18 @@ internal class WalletSettingsModel @Inject constructor( init { getUserWalletUseCase.invoke(params.userWalletId).onRight { wallet -> trackingContextProxy.addContext(wallet) - analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletSettingsScreenOpened()) + modelScope.launch { + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(wallet.walletId) + .first() + .accounts + .size + } else { + null + } + val event = WalletSettingsAnalyticEvents.WalletSettingsScreenOpened(accountsCount) + analyticsEventHandler.send(event) + } } fun combineUI(wallet: UserWallet) = combine( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index c9458a072e..0d40442830 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.utils import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.AccountPortfolioItemUMConverter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router @@ -24,6 +25,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.extension.isAccountsSupported import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -46,6 +48,7 @@ internal class AccountItemsDelegate @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountListSortingSaver: AccountListSortingSaver, private val accountsFeatureToggles: AccountsFeatureToggles, + private val analyticsEventHandler: AnalyticsEventHandler, ) { private val userWalletId = paramsContainer.require().userWalletId @@ -72,7 +75,10 @@ internal class AccountItemsDelegate @Inject constructor( ): List = buildList { fun AccountStatus.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM { val accountItemUM = AccountPortfolioItemUMConverter( - onClick = { openAccountDetails(this.account) }, + onClick = { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonOpenExistingAccount()) + openAccountDetails(this.account) + }, appCurrency = appCurrency, accountBalance = this.tokenList.totalFiatBalance, isBalanceHidden = isBalanceHidden, @@ -107,14 +113,22 @@ internal class AccountItemsDelegate @Inject constructor( title = resourceReference(R.string.account_form_title_create), isAddAccountEnabled = isAddAccountEnabled, onAddAccountClick = { - if (isAddAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog() + if (isAddAccountEnabled) { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonAddAccount()) + openAddAccount(userWalletId) + } else { + canNotAddAccountDialog() + } }, ), archivedAccounts = if (isArchivedAccountsEnabled) { BlockUM( text = resourceReference(R.string.account_archived_accounts), iconRes = R.drawable.ic_archive_24, - onClick = { openArchivedAccounts(userWalletId) }, + onClick = { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonArchivedAccounts()) + openArchivedAccounts(userWalletId) + }, ) } else { null diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt index 1f86f48958..52d5a6dbfc 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt @@ -1,7 +1,9 @@ package com.tangem.feature.walletsettings.utils +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.account.usecase.ApplyAccountListSortingUseCase import com.tangem.domain.models.account.AccountId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -24,6 +26,7 @@ import kotlin.time.Duration.Companion.seconds @OptIn(FlowPreview::class) internal class AccountListSortingSaver @Inject constructor( private val applyAccountListSortingUseCase: ApplyAccountListSortingUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ) { @@ -41,6 +44,9 @@ internal class AccountListSortingSaver @Inject constructor( applyAccountListSortingUseCase.invoke(accountIds).onLeft { Timber.e("Error while saving account list sorting: $it") } + .onRight { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.LongtapAccountsOrder()) + } } .launchIn(coroutineScope) } 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 38cf4e0ca2..bf812afdd1 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 @@ -11,6 +11,8 @@ 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 +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -99,6 +101,8 @@ internal class WalletModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val trackingContextProxy: TrackingContextProxy, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val feedFeatureToggle: FeedFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -293,8 +297,19 @@ internal class WalletModel @Inject constructor( modelScope.launch { val hasMobileWallet = userWalletsListRepository.userWalletsSync() .any { it is UserWallet.Hot } + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(selectedWallet.walletId) + .first() + .accounts + .size + } else { + null + } analyticsEventsHandler.send( - WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(hasMobileWallet), + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + hasMobileWallet = hasMobileWallet, + accountsCount = accountsCount, + ), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index ca2353b273..747e1364f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -196,11 +196,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() + analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens()) accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) } override fun onAccountCollapseClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() + analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountHideTokens()) accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId) } 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 5d679b0b59..317c4582c9 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 @@ -57,9 +57,13 @@ sealed class WalletScreenAnalyticsEvent { data class ScreenOpened( private val hasMobileWallet: Boolean, + private val accountsCount: Int?, ) : MainScreen( event = "Screen opened", - params = mapOf("Mobile Wallet" to if (hasMobileWallet) "Yes" else "No"), + params = buildMap { + put("Mobile Wallet", if (hasMobileWallet) "Yes" else "No") + if (accountsCount != null) put("Accounts Count", accountsCount.toString()) + }, ) data class NoticeFinishActivation( 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 76072cedae..e0a0182370 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 @@ -31,6 +31,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.walletconnect.WcAnalyticAccountEvents import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.Unknown @@ -319,6 +320,17 @@ internal class WcPairModel @Inject constructor( val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull() val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value val account = selectedPortfolio?.second?.account + + modelScope.launch { + if (selectorController.isAccountModeSync() && account != null) { + val derivationIndex = when (account) { + is Account.CryptoPortfolio -> account.derivationIndex.value + } + analytics.send(WcAnalyticAccountEvents.PairButtonConnect(derivationIndex)) + } else { + analytics.send(WcAnalyticEvents.PairButtonConnect()) + } + } wcPairUseCase.approve( WcSessionApprove( wallet = wallet, From 6d198906281c8623947c3c98225300841310a1ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 08:04:53 +0000 Subject: [PATCH 067/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d167ffb2eb..8c10bcaf9d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "develop-1317" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +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 3bd91a49a128d5d6e827d2efd9a058d486b2ac39 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 13:20:48 +0500 Subject: [PATCH 068/130] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Dialogs.kt | 3 ++- .../ui/CreateWalletSelectionContent.kt | 16 ++++++++++++---- .../hotwallet/accesscode/AccessCodeModel.kt | 5 +++++ .../features/hotwallet/common/ui/OptionBlock.kt | 9 +++++---- .../ui/CreateHardwareWalletContent.kt | 3 +++ .../ui/CreateMobileWalletContent.kt | 3 +++ .../start/ui/ManualBackupStartContent.kt | 10 ++++++---- .../upgradewallet/ui/UpgradeWalletContent.kt | 3 +++ .../ui/WalletHardwareBackupContent.kt | 2 ++ 9 files changed, 41 insertions(+), 13 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index f9baae94a7..faeb20ff2f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -308,13 +308,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun DialogButtons( confirmButton: DialogButtonUM, dismissButton: DialogButtonUM?, modifier: Modifier = Modifier, ) { - Row( + FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( space = TangemTheme.dimens.spacing4, diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 07cdb0689e..cddcad417a 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,6 +25,7 @@ import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -72,6 +75,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, @@ -114,6 +118,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun WalletBlock( title: String, @@ -138,12 +143,11 @@ private fun WalletBlock( vertical = 12.dp, ), ) { - Row( + FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - modifier = Modifier - .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, @@ -178,6 +182,7 @@ private fun WalletBlock( private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) { Row( modifier = modifier, + verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(TangemTheme.dimens.size16), @@ -223,6 +228,8 @@ private fun AlreadyHaveTangemWalletBlock( text = stringResourceSafe(R.string.wallet_add_hardware_purchase), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, ) SecondaryButton( @@ -244,7 +251,7 @@ private fun PreviewCreateWalletContent() { onBackClick = { }, blocks = persistentListOf( CreateWalletSelectionUM.Block( - title = resourceReference(R.string.wallet_create_hardware_title), + title = stringReference("Hardware wallet very long title"), titleLabel = LabelUM( text = resourceReference(R.string.common_recommended), style = LabelStyle.ACCENT, @@ -279,6 +286,7 @@ private fun PreviewCreateWalletContent() { onClick = { }, ), ), + shouldShowAlreadyHaveWallet = true, onBuyClick = { }, ), ) 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 595dff3477..0e1efd5301 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 @@ -154,6 +154,11 @@ internal class AccessCodeModel @Inject constructor( title = resourceReference(R.string.access_code_alert_validation_ok), onClick = ::setNewCode, ), + onDismissRequest = { + uiState.update { currentState -> + currentState.copy(onAccessCodeChange = ::onAccessCodeChange) + } + }, ), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index 6f7f584bfb..bcc2a2c440 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -3,7 +3,8 @@ package com.tangem.features.hotwallet.common.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -19,6 +20,7 @@ import com.tangem.core.ui.res.TangemTheme private const val DISABLED_COLORS_ALPHA = 0.5f +@OptIn(ExperimentalLayoutApi::class) @Suppress("LongParameterList") @Composable internal fun OptionBlock( @@ -44,12 +46,11 @@ internal fun OptionBlock( } .padding(16.dp), ) { - Row( + FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - modifier = Modifier - .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt index 8f2ce41cfd..095790c30a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createhardwarewallet.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.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -42,6 +44,7 @@ internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index 99f127b9bf..e6916bdcfd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createmobilewallet.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.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -41,6 +43,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, 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 fe37f62480..8d2a207aa8 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 @@ -8,7 +8,6 @@ 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 @@ -25,7 +24,7 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) { - Box( + Column( modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() @@ -36,7 +35,11 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi bottom = 16.dp, ), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + ) { Text( modifier = Modifier .fillMaxWidth() @@ -86,7 +89,6 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi PrimaryButton( modifier = Modifier - .align(Alignment.BottomCenter) .fillMaxWidth() .padding(top = 16.dp), text = stringResourceSafe(R.string.common_continue), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt index 515d01c482..a643ee75e0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.upgradewallet.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.Icon import androidx.compose.material3.Text @@ -44,6 +46,7 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt index 9635f67b45..8e4eafbf78 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt @@ -117,6 +117,8 @@ private fun PurchaseBlock(onBuyClick: () -> Unit, modifier: Modifier = Modifier) text = stringResourceSafe(R.string.wallet_add_hardware_purchase), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, ) SecondaryButton( From 626d0914a0cf29d31a3366c1c985bb61b5e0b371 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 10:48:01 +0100 Subject: [PATCH 069/130] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 8 + common/ui-markets/.gitignore | 1 + common/ui-markets/build.gradle.kts | 29 ++ .../ui/markets/MarketChartLoadingError.kt | 39 ++ .../common/ui/markets}/MarketListItem.kt | 8 +- .../ui/markets}/MarketsListItemPlaceholder.kt | 2 +- .../ui/markets/models}/MarketsListItemUM.kt | 2 +- .../MarketChartListItemPreviewDataProvider.kt | 6 +- .../com/tangem/common/ui/news/ArticleCard.kt | 7 +- .../tangem/common/ui/news/ArticleConfigUM.kt | 3 +- core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values-ja/strings.xml | 6 +- core/res/src/main/res/values-ru/strings.xml | 30 +- core/res/src/main/res/values/strings.xml | 36 +- core/ui/build.gradle.kts | 2 + .../core/ui/utils/DateTimeFormatters.kt | 18 + .../com/tangem/core/ui/utils/DateUtils.kt | 47 +++ .../com/tangem/core/ui/utils/DateUtilsTest.kt | 155 +++++++ .../markets/GetTopFiveMarketTokenUseCase.kt | 23 ++ features/feed/impl/build.gradle.kts | 1 + ...ponentModule.kt => FeedComponentModule.kt} | 2 +- .../converter/MarketsTokenItemConverter.kt | 159 ++++++++ .../feed/model/feed/FeedComponentModel.kt | 187 ++++++++- .../tangem/features/feed/ui/feed/FeedList.kt | 79 ++-- .../features/feed/ui/feed/FeedListLoading.kt | 2 +- .../preview/FeedListPreviewDataProvider.kt | 16 +- .../features/feed/ui/feed/state/FeedListUM.kt | 4 +- .../feed/state/FeedMarketsBatchFlowManager.kt | 378 ++++++++++++++++++ .../ui/feed/state/TrendingNewsStateFactory.kt | 41 +- .../feed/ui/market/state/MarketsListUM.kt | 1 + settings.gradle.kts | 1 + 31 files changed, 1201 insertions(+), 95 deletions(-) create mode 100644 common/ui-markets/.gitignore create mode 100644 common/ui-markets/build.gradle.kts create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets}/MarketListItem.kt (97%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets}/MarketsListItemPlaceholder.kt (98%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models}/MarketsListItemUM.kt (96%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets}/preview/MarketChartListItemPreviewDataProvider.kt (95%) create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt rename features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/{ComponentModule.kt => FeedComponentModule.kt} (92%) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt 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 ef41aa0007..d6bfbdb63a 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 @@ -37,6 +37,14 @@ object MarketsDomainModule { return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository) } + @Provides + @Singleton + fun provideGetTopFiveMarketTokenUseCase( + marketsTokenRepository: MarketsTokenRepository, + ): GetTopFiveMarketTokenUseCase { + return GetTopFiveMarketTokenUseCase(marketsTokenRepository = marketsTokenRepository) + } + @Provides @Singleton fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase { diff --git a/common/ui-markets/.gitignore b/common/ui-markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/ui-markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/ui-markets/build.gradle.kts b/common/ui-markets/build.gradle.kts new file mode 100644 index 0000000000..b83e4298e2 --- /dev/null +++ b/common/ui-markets/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui.markets" +} + +dependencies { + /** Project - Core */ + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Project - Common */ + implementation(projects.common.uiCharts) + implementation(projects.common.ui) + + /** Project - Domain */ + implementation(projects.domain.models) + + implementation(deps.lifecycle.compose) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + implementation(deps.kotlin.immutable.collections) +} \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt new file mode 100644 index 0000000000..2cbcf9ad7c --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt @@ -0,0 +1,39 @@ +package com.tangem.common.ui.markets + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun MarketChartLoadingError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(vertical = 32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.common_unable_to_load), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH(12.dp) + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.try_to_load_data_again_button_title), + onClick = onRetryClick, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt similarity index 97% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt index 9f011aa99a..f2931ff828 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.components +package com.tangem.common.ui.markets import android.content.res.Configuration import androidx.compose.foundation.background @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.R import com.tangem.core.ui.components.* @@ -32,13 +34,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MarketsTestTags import com.tangem.core.ui.windowsize.WindowSizeType -import com.tangem.features.feed.ui.market.preview.MarketChartListItemPreviewDataProvider -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.utils.StringsSigns.MINUS import kotlin.random.Random @Composable -internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { +fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { MarketsListItemContent( modifier = modifier .fillMaxWidth() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt similarity index 98% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt index ab52d344f0..3e11c3c039 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.components +package com.tangem.common.ui.markets import android.content.res.Configuration import androidx.compose.foundation.background diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt similarity index 96% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt index 5e518373dc..9d48452aef 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.state +package com.tangem.common.ui.markets.models import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt similarity index 95% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt index 98dd938ce2..7cac4e8314 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt @@ -1,15 +1,15 @@ -package com.tangem.features.feed.ui.market.preview +package com.tangem.common.ui.markets.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf @Suppress("MagicNumber") -internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( +class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( MarketsListItemUM( id = CryptoCurrency.RawID("1"), diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 70d50e3b81..91e26bb895 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference +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 @@ -87,7 +88,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) { ArticleInfo( score = articleConfigUM.score, - createdAt = articleConfigUM.createdAt, + createdAt = articleConfigUM.createdAt.resolveReference(), ) SpacerH(32.dp) @@ -104,7 +105,7 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) { Column(modifier = Modifier.padding(12.dp)) { ArticleInfo( score = articleConfigUM.score, - createdAt = articleConfigUM.createdAt, + createdAt = articleConfigUM.createdAt.resolveReference(), ) SpacerH(8.dp) @@ -182,7 +183,7 @@ private fun ArticleCardsPreview() { id = 1, title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", score = 9.5f, - createdAt = "1h ago", + createdAt = TextReference.Str("1h ago"), isTrending = true, tags = tags, isViewed = false, diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt index 46e616eaab..46714a0ba8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt @@ -1,13 +1,14 @@ package com.tangem.common.ui.news import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableSet data class ArticleConfigUM( val id: Int, val title: String, val score: Float, - val createdAt: String, + val createdAt: TextReference, val isTrending: Boolean, val tags: ImmutableSet, val isViewed: Boolean, diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index a009fa485f..0dc6af1380 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -849,6 +849,9 @@ 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 + Los geht\'s + Nur für kurze Zeit! + 1+1: Beim Kauf einer Wallet erhältst Du 50% Rabatt 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 94e4382158..82aca73289 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -848,8 +848,8 @@ ウォレットごとに$10相当のBTCをプレゼント\nお早めに! ブラックフライデー:最大30% オフ 今すぐチェック - 理想のTangemセットを揃えよう。期間限定。 - 1+1:1つ購入で、2つ目が50%オフ + 期間限定! + 1+1:ウォレット1つ購入で、2つ目が50%オフ 今すぐ参加 コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 友達への紹介で報酬を獲得しよう! @@ -1391,7 +1391,7 @@ 取引を表示 サービス手数料 手数料 - カードを紛失したり盗まれたりしても、資金を安全に保てます。一時停止はいつでも解除できます。 + 資金を安全に保管します。凍結はいつでも解除できます。 カードを一時停止しますか? カードを凍結できませんでした。しばらくしてからもう一度お試しください。 一時停止 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b5587d3945..0ad7e694bf 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -16,8 +16,10 @@ Код доступа Превышен лимит %1$s аккаунтов. Архивируйте, чтобы создать новый. Невозможно добавить новый аккаунт. + Аккаунт заархивирован Архивированные аккаунты Восстановить + Вы собираетесь восстановить \"%1$s\". Восстановить аккаунт Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить. Невозможно восстановить аккаунт @@ -26,12 +28,14 @@ Этот аккаунт участвует в реферальной программе Этот аккаунт не может быть архивирован. Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже. + Аккаунт создан Архивный аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... Аккаунт Невозможно отредактировать аккаунт + Аккаунт сохранен Аккаунт для наград Добавить аккаунт Сохранить @@ -46,6 +50,8 @@ Основной аккаунт Лимит %1$s активных аккаунтов превышен. Архивируйте один, чтобы продолжить. Невозможно восстановить аккаунт + Аккаунт восстановлен + Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов. Продолжить Отменить Вы уверены, что хотите создание нового аккаунта? @@ -87,14 +93,17 @@ Токены в сети %1$s не поддерживаются этой картой или кольцом из-за ограничений прошивки. У вас возникли трудности со сканированием карты или кольца? Эта карта не предназначена для работы с этим приложением + Сначала установите код доступа, чтобы включить биометрию. Используйте %1$s, чтобы быстро и безопасно разблокировать кошелёк и выполнять чувствительные действия, например, подписывать транзакции. Для аппаратных кошельков всё ещё требуется карта или кольцо для подписи. Комиссия по-умолчанию Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem Включите биометрическую аутентификацию Отключение %1$s потребует ввода вашего кода доступа для разблокировки приложения и работы с кошельком. + Позже будет необходимо ввести код доступа к вашему кошельку, чтобы мы могли безопасно сохранить его для будущего использования. Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. + Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа. Требуется код доступа Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа. Сохранение кода доступа @@ -144,6 +153,7 @@ Вход по биометрии временно заблокирован. Попробуйте снова через 30 секунд или разблокируйте кошелёк с помощью прикладывания устройства или кода доступа. Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Биометрические данные на вашем устройстве были обновлены. Пожалуйста, выберите свой кошелёк и введите его код доступа, чтобы повторно включить вход по биометрии. Внимание При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. Ошибка активации @@ -299,6 +309,7 @@ Позже Узнать больше Осталось %1$s + Легаси Bitcoin Заблокирован Заблокированные кошельки Основная сеть @@ -311,6 +322,8 @@ %d сетей %d сети + Новый адрес + Новости Далее NFT Нет @@ -363,6 +376,7 @@ Поддержка Поддерживаемые сети Обменять + Tangem условия участия Условиями использования На @@ -391,6 +405,7 @@ Адрес контракта скопирован! Доступные сети Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи. + Деривация принадлежит другому аккаунту Токен добавлен в аккаунт %1$s Адрес контракта Адрес контракта некорректен @@ -527,6 +542,8 @@ ID: %s ID транзакции скопирован Обменяйте любой актив в вашем портфеле на этот токен + Рынок и Новости + В тренде Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта или кольцо? @@ -687,6 +704,7 @@ APY %s Мой портфель Рынок + Зарабатывай с Tangem Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька Потяните вверх или коснитесь поисковой строки, чтобы добавить токен Данные раздела получены из следующих сетей: %s @@ -791,6 +809,8 @@ Активировать режим доходности Обновитесь до версии %1$s, чтобы создать мобильный кошелёк Мобильный кошелек требует %1$s или новее. + Все новости + Будьте в курсе Функция NFC недоступна на вашем устройстве О NFT NFT @@ -1198,6 +1218,7 @@ %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Сумма превышает остаток Для завершения транзакции на указанный адрес необходим тег назначения (мемо). + Требуется Destination Tag Недопустимая сумма Комиссия превышает остаток Отправляемая сумма превышает остаток @@ -1225,6 +1246,8 @@ Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне. Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге. Активация аккаунта + Сетевая комиссия изменилась. Пожалуйста, проверьте новую сумму перед продолжением. + Комиссия сети обновлена Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. @@ -1263,6 +1286,7 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга. Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s Заблокировано + Максимальная сумму: %s Переместить Нативный стейкинг В данный момент нет доступных активных валидаторов для стейкинга. Пожалуйста, попробуйте позже. @@ -1426,7 +1450,7 @@ Разморозить карту? Не удалось разморозить карту, попробуйте еще раз Карта разморожена - Вывод + Вывести Пополнить Способы пополнения Номер @@ -1462,7 +1486,7 @@ Пополните карту любым активом через обмен Реквизиты Разморозить карту - Вывод + Вывести Вывод сейчас недоступен Вы не можете начать обмен или новый вывод, пока не завершится текущий. Вывод выполняется @@ -1598,6 +1622,7 @@ Код доступа Активация аккаунта ПИН не принят. Попробуйте ещё раз или введите другой код. + Разблокировать Выберите способ добавления кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. Создать аппаратный кошелёк @@ -1934,6 +1959,7 @@ Политика комиссий за пополнение Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода. Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. + Высокая сетевая комиссия Историческая доходность Получай до %1$s APY на свой баланс Разрешение для вашего токена в режиме доходности было отозвано. Откройте токен, чтобы выдать разрешение снова. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8c52e119ff..a82169d021 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -584,30 +584,30 @@ Finalize wallet setup Complete setup by securing the app with an access code. If you do, you\'ll need to start over. - Are you sure you want to quit the activation process? - Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. + Are you sure you want to quit the setup process? + Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup Google Drive backup - Create a new secure wallet and transfer your funds for extra protection. + Create a secure wallet and transfer your funds for extra protection. Create new wallet Level up your security with the superior Tangem hardware wallet. Hardware Wallet - Move your current wallet into Tangem Wallet. + Move your current wallet into Tangem. Upgrade current wallet Go to backup Please back up your wallet before creating an access code. - Finish Backup First + Finish backup first Finalize backup first Incomplete Other methods - Manually save your recovery phrase in a secure place and keep it private to protect your funds. + Save your recovery phrase in a secure place and keep it private to protect your funds. Recovery phrase To secure your wallet with an Access Code, complete the backup first. - To upgrade your wallet to hardware, back it up first. + To upgrade to a hardware wallet, complete the backup process. Your private keys are securely encrypted and stored on your phone - Keys are stored in the app - Create or restore your wallet using a recovery phrase — your built-in backup. + Private keys stay on your device + Create or restore your wallet with a recovery phrase. Seed phrase backup Create Mobile Wallet Import existing wallet @@ -620,13 +620,13 @@ Go to backup View backup Forget wallet - Forget Anyway - A backup for this wallet exists. Review it before forgetting to make sure you can recover later. - If you forget this wallet without a backup, you’ll permanently lose access to your funds. - Forget this wallet? + Forget anyway + This wallet has a backup. Ensure you can recover it before forgetting the wallet. + If you forget this wallet without a backup, you\'ll permanently lose access to your funds. + Are you sure you want to forget this wallet? I understand that if I haven\'t backed up my wallet before removing it, I may lose access to it. - I understand that removing my wallet does not delete it—but simply removes it from my device. - No seed phrase is needed anymore. Your Tangem card or ring becomes your secure backup. + I understand that removing my wallet does not delete it, only removes it from my device. + Seed phrase not required. Your Tangem card or ring becomes your secure backup. Backup with Tangem Can’t upgrade. A wallet already exists on this device. Pick another device. This one can’t be used for the upgrade. @@ -639,7 +639,7 @@ Key Migration Scan device Start upgrade - You’re about to upgrade to our hardware wallet. This will keep your assets safe in cold storage. + You\'re about to upgrade to our hardware wallet. It will keep your assets safe in cold storage. Tangem Wallet Upgrade to Hardware Wallet Keep your crypto safe with Tangem\'s top-tier hardware wallet. @@ -726,6 +726,7 @@ Top Gainers Top Losers Trending + Yield Mode Staking is the easiest way to receive rewards on your crypto. %s Earn up to %s APY Token Added @@ -801,7 +802,7 @@ Mobile Wallet requires %1$s or later All news - + %dh ago %dh ago @@ -983,6 +984,7 @@ Other currencies Popular Fiats Search by currency + This transaction has already been processed. No further action is required. Fetching best rates... Instant By using onramp functionality, you agree with provider’s %1$s and %2$s diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3f46d7909d..33e81eb02f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -66,4 +66,6 @@ dependencies { testImplementation(deps.test.junit) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 8a5cae1ae4..728da8c24c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY import org.joda.time.DateTime +import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter import org.joda.time.format.DateTimeFormatterBuilder @@ -98,6 +99,23 @@ object DateTimeFormatters { getBestFormatterBySkeleton("dd.MM.yyyy HH:mm") } + /** + * Local full date formatter (e.g., "dd MMMM, HH:mm") + */ + val localFullDate: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral(' ') + .appendMonthOfYearText() + .appendLiteral(", ") + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + .withZone(DateTimeZone.getDefault()) + } + fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt index e01b50dfb3..9354511464 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -37,4 +37,51 @@ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeForm */ fun Long.formatAsDateTime(formatter: DateTimeFormatter): String { return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter) +} + +/** + * Parses an ISO 8601 date string and compares it to the current UTC time. + * + + * @param now The current date to compare against. + * @return A [FormattedDate] subclass. + */ +@Suppress("MagicNumber") +fun getFormattedDate(createdAt: String, now: DateTime): FormattedDate { + val pastDateUtc = try { + DateTime.parse(createdAt) + } catch (_: Exception) { + return FormattedDate.FullDate(createdAt) + } + + val pastDateLocal = pastDateUtc.withZone(DateTimeZone.getDefault()) + val isToday = pastDateLocal.isToday() + + val diffInMillis = now.millis - pastDateUtc.millis + val diffInMinutes = diffInMillis / (1000 * 60) + val diffInHours = diffInMillis / (1000 * 60 * 60) + + return when { + diffInMinutes < 1 -> FormattedDate.MinutesAgo(1) + diffInMinutes < 60 -> FormattedDate.MinutesAgo(diffInMinutes.toInt()) + diffInHours < 12 && isToday -> FormattedDate.HoursAgo(diffInHours.toInt()) + isToday -> { + val timeString = DateTimeFormatters.timeFormatter.print(pastDateLocal) + FormattedDate.Today(timeString) + } + else -> { + val dateString = DateTimeFormatters.localFullDate.print(pastDateLocal) + FormattedDate.FullDate(dateString) + } + } +} + +/** + * Representing different formatted date representations. + */ +sealed class FormattedDate { + data class MinutesAgo(val minutes: Int) : FormattedDate() + data class HoursAgo(val hours: Int) : FormattedDate() + data class Today(val time: String) : FormattedDate() + data class FullDate(val date: String) : FormattedDate() } \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt new file mode 100644 index 0000000000..6e145b2371 --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt @@ -0,0 +1,155 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateUtilsTest { + + private lateinit var defaultTimeZone: DateTimeZone + + private val now = createDateTime(day = 14, hour = 12) + + @BeforeEach + fun setUp() { + defaultTimeZone = DateTimeZone.getDefault() + DateTimeZone.setDefault(DateTimeZone.forID("Europe/Moscow")) + } + + @AfterEach + fun tearDown() { + DateTimeZone.setDefault(defaultTimeZone) + } + + @Test + fun `should return MinutesAgo when difference is less than 1 minute`() { + val pastDate = now.minusSeconds(30) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should return MinutesAgo when difference is 1 minute`() { + val pastDate = now.minusMinutes(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should return MinutesAgo when difference is 30 minutes`() { + val pastDate = now.minusMinutes(30) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(30) + } + + @Test + fun `should return MinutesAgo when difference is 59 minutes`() { + val pastDate = now.minusMinutes(59).minusSeconds(59) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(59) + } + + @Test + fun `should return HoursAgo when difference is exactly 1 hour`() { + val pastDate = now.minusHours(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java) + Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(1) + } + + @Test + fun `should return HoursAgo when difference is 11 hours`() { + val pastDate = now.minusHours(11) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java) + Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(11) + } + + @Test + fun `should return Today when difference is exactly 12 hours`() { + val pastDate = createDateTime(day = 14, hour = 0) + val createdAt = pastDate.toString() + val nowInTest = createDateTime(day = 14, hour = 12) + val result = getFormattedDate(createdAt, nowInTest) + Truth.assertThat(result).isInstanceOf(FormattedDate.Today::class.java) + Truth.assertThat((result as FormattedDate.Today).time).isEqualTo("03:00") + } + + @Test + fun `should return FullDate when difference is 18 hours and past date is another day`() { + val pastDate = createDateTime(day = 13, hour = 18) + val createdAt = pastDate.toString() + val nowInTest = createDateTime(day = 14, hour = 12) + val result = getFormattedDate(createdAt, nowInTest) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate when difference is exactly 24 hours`() { + val pastDate = now.minusDays(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate when difference is 2 days`() { + val pastDate = now.minusDays(2) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate with original string when format has wrong date separator`() { + val wrongFormat = "2025/10/14T12:00:00.000Z" + val result = getFormattedDate(wrongFormat, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + Truth.assertThat((result as FormattedDate.FullDate).date).isEqualTo(wrongFormat) + } + + @Test + fun `should handle future dates correctly`() { + val futureDate = now.plusHours(1) + val createdAt = futureDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should handle edge case of exactly 0 milliseconds difference`() { + val createdAt = now.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + private fun createDateTime(day: Int, hour: Int): DateTime { + return DateTime( + /* year = */ 2025, + /* monthOfYear = */ 10, + /* dayOfMonth = */ day, + /* hourOfDay = */ hour, + /* minuteOfHour = */ 0, + /* secondOfMinute = */ 0, + /* millisOfSecond = */ 0, + /* zone = */ DateTimeZone.UTC, + ) + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt new file mode 100644 index 0000000000..c81d7fcbfe --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.markets + +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTopFiveMarketTokenUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + operator fun invoke( + batchingContext: TokenListBatchingContext, + order: TokenMarketListConfig.Order, + ): TokenListBatchFlow { + return marketsTokenRepository.getTokenListFlow( + batchingContext = batchingContext, + firstBatchSize = DEFAULT_BATCH_SIZE, + nextBatchSize = NEXT_BATCH_SIZE, + ) + } + + companion object { + private const val DEFAULT_BATCH_SIZE = 5 + private const val NEXT_BATCH_SIZE = 0 + } +} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index cadd191a0e..9d6659788e 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { implementation(projects.common.ui) implementation(projects.common.uiCharts) implementation(projects.common.routing) + implementation(projects.common.uiMarkets) /* Libs */ implementation(projects.libs.crypto) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.kt similarity index 92% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.kt index 5c06261458..7d5c10f82d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.kt @@ -10,7 +10,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal interface ComponentModule { +internal interface FeedComponentModule { @Binds @Singleton diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt new file mode 100644 index 0000000000..b513310fb1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -0,0 +1,159 @@ +package com.tangem.features.feed.model.converter + +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.market.state.MarketsListUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class MarketsTokenItemConverter( + private val currentTrendInterval: MarketsListUM.TrendInterval, + private val appCurrency: AppCurrency, +) : Converter { + + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false) + + override fun convert(value: TokenMarket): MarketsListItemUM { + return MarketsListItemUM( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + ratingPosition = value.marketRating?.toString(), + marketCap = value.getMarketCap(), + iconUrl = value.imageUrlLarge, + price = value.getCurrentPrice(), + trendPercentText = value.getTrendPercent(), + trendType = value.getTrendType(), + chartData = value.getChartData(), + isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.stakingRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, + updateTimestamp = value.updateTimestamp, + ) + } + + fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM { + require(prev.id == new.id) { + "Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]" + } + + return prevUI.copy( + name = new.name, + currencySymbol = new.symbol, + ratingPosition = new.marketRating?.toString(), + marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, + iconUrl = new.imageUrlLarge, + price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) { + new.getCurrentPrice( + prev = prev, + ) + }, + trendPercentText = ifChanged( + prev.tokenQuotesShort, + new.tokenQuotesShort, + prevUI.trendPercentText, + ) { new.getTrendPercent() }, + trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, + ) + } + + private inline fun ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R { + return if (force || prev != new) change(new) else prevR + } + + private fun TokenMarket.getMarketCap(): String? { + val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null + + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } + } + + private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { + val prevPrice = prev?.tokenQuotesShort?.currentPrice + + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } + + val changeType = if (prevPrice != null) { + if (tokenQuotesShort.currentPrice > prevPrice) { + PriceChangeType.UP + } else { + PriceChangeType.DOWN + } + } else { + null + } + + return MarketsListItemUM.Price( + text = priceText, + changeType = changeType, + ) + } + + private fun TokenMarket.getChartData(): MarketChartRawData? { + val chart = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenCharts.h24 + MarketsListUM.TrendInterval.D7 -> tokenCharts.week + MarketsListUM.TrendInterval.M1 -> tokenCharts.month + } + + return chart?.let { ct -> + priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = ct.priceY.toImmutableList(), + x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), + ) + } + } + + @Suppress("MagicNumber") + private fun TokenMarket.getTrendType(): PriceChangeType { + val percent = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent + } + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } + + private fun TokenMarket.getTrendPercent(): String { + val percent = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent + } + + return percent.format { percent() } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 5445a03eed..6467b4cc3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -1,11 +1,16 @@ package com.tangem.features.feed.model.feed import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.impl.R @@ -13,10 +18,12 @@ import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentHashMap -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -28,11 +35,30 @@ internal class FeedComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, + getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { private val _state = MutableStateFlow(initialState()) val state = _state.asStateFlow() + private var quotesUpdateJob: Job? = null + + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val marketsBatchFlowManager = FeedMarketsBatchFlowManager( + getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase, + currentAppCurrency = Provider { currentAppCurrency.value }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) { SearchBarStateFactory( currentStateProvider = Provider { _state.value }, @@ -55,8 +81,45 @@ internal class FeedComponentModel @Inject constructor( _state.update { feedListUM -> feedListUM.copy( searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + feedListCallbacks = feedListUM.feedListCallbacks.copy( + onSortTypeClick = ::onSortTypeClick, + ), ) } + + modelScope.launch(dispatchers.default) { + combine( + marketsBatchFlowManager.itemsByOrder, + marketsBatchFlowManager.loadingStatesByOrder, + marketsBatchFlowManager.errorStatesByOrder, + ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder -> + updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder) + val currentSortType = _state.value.marketChartConfig.currentSortByType + val items = itemsByOrder[currentSortType] + val isLoading = loadingStatesByOrder[currentSortType] == true + if (items != null && items.isNotEmpty() && !isLoading) { + val order = currentSortType.toOrder() + marketsBatchFlowManager.loadCharts(order) + } + }.collect() + } + + modelScope.launch(dispatchers.default) { + currentAppCurrency.drop(1).collect { + marketsBatchFlowManager.reloadAll() + } + } + + modelScope.launch(dispatchers.default) { + TokenMarketListConfig.Order.entries.forEach { order -> + marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey -> + marketsBatchFlowManager.loadCharts(order) + if (batchKey == 0) { + startQuotesUpdateTimer() + } + } + } + } } private suspend fun subscribeOnTrendingNews() { @@ -88,10 +151,10 @@ internal class FeedComponentModel @Inject constructor( marketChartConfig = MarketChartConfig( marketCharts = buildMap { SortByTypeUM.entries.forEach { - put(it, MarketChartUM.Loading) - } // TODO in [REDACTED_TASK_KEY] add correct sorting + put(it, MarketChartUM.LoadingError(onRetryClicked = marketsBatchFlowManager::reloadAll)) + } }.toPersistentHashMap(), - currentSortByType = SortByTypeUM.TopGainers, + currentSortByType = SortByTypeUM.Trending, ), ) } @@ -100,4 +163,116 @@ internal class FeedComponentModel @Inject constructor( val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) } + + private fun updateMarketCharts( + itemsByOrder: Map>, + loadingStatesByOrder: Map, + errorStatesByOrder: Map, + ) { + _state.update { currentState -> + val newMarketCharts = buildMap { + SortByTypeUM.entries.forEach { sortByType -> + val items = itemsByOrder[sortByType] ?: persistentListOf() + val isLoading = loadingStatesByOrder[sortByType] == true + val hasError = errorStatesByOrder[sortByType] == true + + when { + hasError -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = { + marketsBatchFlowManager.reloadAll() + }, + ), + ) + } + isLoading -> { + put(sortByType, MarketChartUM.Loading) + } + items.isEmpty() -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = { + marketsBatchFlowManager.reloadAll() + }, + ), + ) + } + else -> { + put( + sortByType, + MarketChartUM.Content( + items = items, + sortChartConfig = SortChartConfigUM( + sortByType = sortByType, + isSelected = sortByType == currentState.marketChartConfig.currentSortByType, + ), + ), + ) + } + } + } + }.toPersistentHashMap() + + currentState.copy( + marketChartConfig = currentState.marketChartConfig.copy( + marketCharts = newMarketCharts, + ), + ) + } + } + + private fun onSortTypeClick(sortByType: SortByTypeUM) { + _state.update { currentState -> + val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> + when (chart) { + is MarketChartUM.Content -> { + chart.copy( + sortChartConfig = chart.sortChartConfig.copy( + isSelected = chartSortType == sortByType, + ), + ) + } + else -> chart + } + } + + currentState.copy( + marketChartConfig = currentState.marketChartConfig.copy( + currentSortByType = sortByType, + marketCharts = updatedCharts.toPersistentHashMap(), + ), + ) + } + modelScope.launch(dispatchers.default) { + marketsBatchFlowManager.loadCharts(sortByType.toOrder()) + } + } + + private fun startQuotesUpdateTimer() { + quotesUpdateJob?.cancel() + quotesUpdateJob = modelScope.launch { + while (true) { + delay(DELAY_TO_FETCH_QUOTES) + marketsBatchFlowManager.updateQuotes() + } + } + } + + private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { + return when (this) { + SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating + SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending + SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers + SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers + SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers + SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + } + } + + companion object { + private const val DELAY_TO_FETCH_QUOTES = 60_000L + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 82cde520e1..6a6b6b2b3f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -14,7 +14,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -29,6 +31,10 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.markets.MarketChartLoadingError +import com.tangem.common.ui.markets.MarketsListItem +import com.tangem.common.ui.markets.MarketsListItemPlaceholder +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleCard import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.R @@ -49,9 +55,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState import com.tangem.features.feed.ui.feed.state.* -import com.tangem.features.feed.ui.market.components.MarketsListItem -import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @Composable @@ -101,10 +104,12 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { SpacerH(32.dp) - MarketBlock( - marketChartConfig = state.marketChartConfig, - feedListCallbacks = state.feedListCallbacks, - ) + state.marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { marketChartUM -> + MarketBlock( + marketChart = marketChartUM, + feedListCallbacks = state.feedListCallbacks, + ) + } NewsBlock( news = state.news, @@ -120,44 +125,43 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { } @Composable -private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { - if (marketChartConfig.marketCharts.isNotEmpty()) { - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, - ) - - SpacerH(12.dp) - - marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { chart -> - Charts( - onItemClick = feedListCallbacks.onMarketItemClick, - modifier = Modifier.padding(horizontal = 16.dp), - marketChart = chart, +private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, ) - } - SpacerH(32.dp) - } + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + ) + + SpacerH(12.dp) + + Charts( + onItemClick = feedListCallbacks.onMarketItemClick, + modifier = Modifier.padding(horizontal = 16.dp), + marketChart = marketChart, + ) + SpacerH(32.dp) } @Composable private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { + val onSeeAllClick by rememberUpdatedState { + feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) + } if (marketChartConfig.marketCharts.isNotEmpty()) { Header( title = { Text( - text = stringResourceSafe(R.string.markets_common_title), + text = stringResourceSafe(R.string.markets_pulse_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, + onSeeAllClick = { onSeeAllClick() }, ) LazyRow( @@ -330,11 +334,7 @@ private fun Charts( modifier = modifier, colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { + Column(modifier = Modifier.fillMaxWidth()) { when (marketChart) { MarketChartUM.Loading -> { repeat(DEFAULT_CHART_SIZE_IN_MARKET) { @@ -342,7 +342,10 @@ private fun Charts( } } is MarketChartUM.LoadingError -> { - // TODO will be created in [REDACTED_TASK_KEY] + MarketChartLoadingError( + modifier = Modifier.fillMaxWidth(), + onRetryClick = marketChart.onRetryClicked, + ) } is MarketChartUM.Content -> { marketChart.items.fastForEach { chart -> diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt index 69f94e61e6..0d936738f5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -8,13 +8,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.common.ui.news.DefaultLoadingArticle import com.tangem.common.ui.news.TrendingLoadingArticle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder @Composable internal fun MarketLoadingBlock() { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 747d3d49bc..9cf0dea267 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -1,17 +1,16 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.ui.feed.state.* -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM import kotlinx.collections.immutable.* @@ -94,7 +93,6 @@ internal object FeedListPreviewDataProvider { ): MarketChartUM.Content { return MarketChartUM.Content( items = items, - triggerScrollReset = consumedEvent(), sortChartConfig = SortChartConfigUM( sortByType = sortByType, isSelected = isSelected, @@ -107,7 +105,7 @@ internal object FeedListPreviewDataProvider { id = 1, title = "Bitcoin ETF reaches new highs, institutions pile in", score = 0.82f, - createdAt = "2h ago", + createdAt = TextReference.Str("Yesterday"), isTrending = true, tags = createArticleTags(), isViewed = false, @@ -116,7 +114,7 @@ internal object FeedListPreviewDataProvider { id = 2, title = "Layer 2 networks battle for dominance amid fee wars", score = 0.71f, - createdAt = "4h ago", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = true, @@ -125,7 +123,7 @@ internal object FeedListPreviewDataProvider { id = 3, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -134,7 +132,7 @@ internal object FeedListPreviewDataProvider { id = 4, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -143,7 +141,7 @@ internal object FeedListPreviewDataProvider { id = 5, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -152,7 +150,7 @@ internal object FeedListPreviewDataProvider { id = 6, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 60f842ea9b..519472ae49 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -1,10 +1,9 @@ package com.tangem.features.feed.ui.feed.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.event.StateEvent -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -46,7 +45,6 @@ internal sealed interface MarketChartUM { data class Content( val items: ImmutableList, - val triggerScrollReset: StateEvent, val sortChartConfig: SortChartConfigUM, ) : MarketChartUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt new file mode 100644 index 0000000000..bdda39948d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt @@ -0,0 +1,378 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.features.feed.model.converter.MarketsTokenItemConverter +import com.tangem.features.feed.ui.market.state.MarketsListUM +import com.tangem.features.feed.ui.market.state.SortByTypeUM +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class FeedMarketsBatchFlowManager( + private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, + private val currentAppCurrency: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val managersByOrder = TokenMarketListConfig.Order.entries.associateWith { order -> + createManagerForOrder(order) + } + + val itemsByOrder: StateFlow>> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.uiItems?.map { items -> order to items } + }, + ) { itemsList -> + itemsList.associate { (order, items) -> + val sortByType = order.toSortByTypeUM() + sortByType to items + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + val loadingStatesByOrder: StateFlow> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.isLoading?.map { isLoading -> order to isLoading } + }, + ) { loadingStatesList -> + loadingStatesList.associate { (order, isLoading) -> + val sortByType = order.toSortByTypeUM() + sortByType to isLoading + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + val errorStatesByOrder: StateFlow> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.hasError?.map { hasError -> order to hasError } + }, + ) { errorStatesList -> + errorStatesList.associate { (order, hasError) -> + val sortByType = order.toSortByTypeUM() + sortByType to hasError + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + init { + managersByOrder.values.forEach { manager -> + manager.reload(currentAppCurrency().code) + } + } + + private fun createManagerForOrder(order: TokenMarketListConfig.Order): SingleOrderManager { + val actionsFlow = MutableSharedFlow>() + + val batchFlow = getTopFiveMarketTokenUseCase( + batchingContext = TokenListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + order = order, + ) + + return SingleOrderManager( + order = order, + actionsFlow = actionsFlow, + batchFlow = batchFlow, + currentAppCurrency = currentAppCurrency, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + fun reloadAll() { + managersByOrder.values.forEach { manager -> + manager.reload(currentAppCurrency().code) + } + } + + fun updateQuotes() { + managersByOrder.values.forEach { manager -> + manager.updateQuotes(currentAppCurrency().code) + } + } + + fun loadCharts(order: TokenMarketListConfig.Order) { + managersByOrder[order]?.loadCharts() + } + + fun getOnLastBatchLoadedSuccessFlow(order: TokenMarketListConfig.Order): Flow? { + return managersByOrder[order]?.onLastBatchLoadedSuccess + } + + private class SingleOrderManager( + val order: TokenMarketListConfig.Order, + private val actionsFlow: MutableSharedFlow>, + private val batchFlow: TokenListBatchFlow, + private val currentAppCurrency: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, + ) { + private val updateStateJob = JobHolder() + private val resultBatches = MutableStateFlow(ResultBatches()) + private val uiBatches = resultBatches.map { it.uiBatches } + + val uiItems: StateFlow> = + uiBatches + .map { batches -> + batches.asSequence() + .map { it.data } + .flatten() + .toImmutableList() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = persistentListOf(), + ) + + val isLoading = batchFlow.state + .map { state -> + when (state.status) { + is PaginationStatus.InitialLoading -> true + is PaginationStatus.NextBatchLoading -> true + else -> false + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = false, + ) + + val hasError = batchFlow.state + .map { state -> + when (val status = state.status) { + is PaginationStatus.InitialLoadingError -> true + is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error + else -> false + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = false, + ) + + val onLastBatchLoadedSuccess = batchFlow.state + .distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size } + .mapNotNull { batchListState -> + when (val status = batchListState.status) { + is PaginationStatus.Paginating -> { + if (status.lastResult is BatchFetchResult.Success) { + batchListState.data.lastOrNull()?.key + } else { + null + } + } + is PaginationStatus.EndOfPagination -> { + batchListState.data.lastOrNull()?.key + } + else -> null + } + } + + init { + batchFlow.state + .map { it.data } + .distinctUntilChanged { a, b -> + a.size == b.size && + a.map { it.key } == b.map { it.key } && + a.map { it.data }.flatten() == b.map { it.data }.flatten() + } + .onEach { + coroutineScope { + launch { + updateState(it) + }.saveIn(updateStateJob) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private suspend fun updateState(newList: List>>, forceUpdate: Boolean = false) = + withContext(dispatchers.default) { + resultBatches.update { resultBatches -> + val items = resultBatches.uiBatches + val previousList = resultBatches.processedItems + + val converter = MarketsTokenItemConverter( + currentTrendInterval = MarketsListUM.TrendInterval.H24, + appCurrency = currentAppCurrency(), + ) + + if (newList.isEmpty()) { + return@update ResultBatches(processedItems = emptyList()) + } + + val isInitialLoading = + forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key + + val outItems = if (isInitialLoading) { + newList.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + // As nextBatchSize = 0, we only have one batch, but keep the logic for safety + if (previousList.size != newList.size) { + val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = newList.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = newList[batchIndex] + if (prevBatch == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data.getOrNull(index) + val newItem = newBatch.data.getOrNull(index) + if (prevItem != null && newItem != null) { + converter.update(prevItem, marketsListItemUM, newItem) + } else { + newItem?.let { converter.convert(it) } ?: marketsListItemUM + } + }, + ) + } + } + } + + currentCoroutineContext().ensureActive() + + ResultBatches( + uiBatches = outItems, + processedItems = newList, + ) + } + } + + fun reload(fiatPriceCurrency: String) { + modelScope.launch(dispatchers.default) { + resultBatches.value = ResultBatches() + actionsFlow.emit( + BatchAction.Reload( + requestParams = TokenMarketListConfig( + fiatPriceCurrency = fiatPriceCurrency, + searchText = null, + priceChangeInterval = TokenMarketListConfig.Interval.H24, + order = order, + ), + ), + ) + } + } + + fun updateQuotes(fiatPriceCurrency: String) { + modelScope.launch(dispatchers.default) { + actionsFlow.emit( + BatchAction.CancelUpdates { + it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes + }, + ) + + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchFlow + .state + .value + .data + .map { it.key } + .toSet(), + updateRequest = TokenMarketUpdateRequest.UpdateQuotes( + currencyId = fiatPriceCurrency, + ), + async = true, + operationId = "update quotes", + ), + ) + } + } + + fun loadCharts() { + modelScope.launch(dispatchers.default) { + val currentData = batchFlow.state.value.data + val alreadyLoadedChartsBatchKeys = currentData + .filter { batch -> + val first = batch.data.firstOrNull() ?: return@filter false + first.tokenCharts.h24 != null + } + .map { it.key } + .toSet() + + val batchesKeysToLoad = currentData.map { it.key }.toSet().minus(alreadyLoadedChartsBatchKeys) + + if (batchesKeysToLoad.isNotEmpty()) { + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchesKeysToLoad, + updateRequest = TokenMarketUpdateRequest.UpdateChart( + interval = TokenMarketListConfig.Interval.H24, + currency = currentAppCurrency().code, + ), + async = true, + operationId = batchesKeysToLoad.toString() + "h24", + ), + ) + } + } + } + + private data class ResultBatches( + val uiBatches: List>> = emptyList(), + val processedItems: List>>? = null, + ) + } + + private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM { + return when (this) { + TokenMarketListConfig.Order.ByRating -> SortByTypeUM.Rating + TokenMarketListConfig.Order.Trending -> SortByTypeUM.Trending + TokenMarketListConfig.Order.Buyers -> SortByTypeUM.ExperiencedBuyers + TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers + TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers + TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt index 52bc5e2c2f..f096546b37 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -4,12 +4,19 @@ import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.FormattedDate +import com.tangem.core.ui.utils.getFormattedDate import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.news.ShortArticle +import com.tangem.features.feed.impl.R import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet +import org.joda.time.DateTime internal class TrendingNewsStateFactory( private val currentStateProvider: Provider, @@ -47,7 +54,7 @@ internal class TrendingNewsStateFactory( ) }, ).toPersistentSet(), - createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + createdAt = mapFormattedDate(article.createdAt), isViewed = article.viewed, ) }, @@ -72,7 +79,7 @@ internal class TrendingNewsStateFactory( ) }, ).toPersistentSet(), - createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + createdAt = mapFormattedDate(article.createdAt), isViewed = article.viewed, ) }.toPersistentList(), @@ -80,4 +87,34 @@ internal class TrendingNewsStateFactory( ), ) } + + private fun mapFormattedDate(createdAt: String): TextReference { + val formattedDate = getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + return when (formattedDate) { + is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) + is FormattedDate.HoursAgo -> TextReference.PluralRes( + id = R.plurals.news_published_hours_ago, + count = formattedDate.hours, + formatArgs = wrappedList(formattedDate.hours), + ) + is FormattedDate.MinutesAgo -> TextReference.PluralRes( + id = R.plurals.news_published_minutes_ago, + count = formattedDate.minutes, + formatArgs = wrappedList(formattedDate.minutes), + ) + is FormattedDate.Today -> TextReference.Combined( + refs = WrappedList( + data = listOf( + TextReference.Res(R.string.common_today), + TextReference.Str(StringsSigns.COMA_SIGN), + TextReference.Str(StringsSigns.WHITE_SPACE), + TextReference.Str(formattedDate.time), + ), + ), + ) + } + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt index ea7e5f740a..27e1c0daaf 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.market.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM diff --git a/settings.gradle.kts b/settings.gradle.kts index f6e74fac62..6a05fead77 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -175,6 +175,7 @@ include(":common:routing") include(":common:test") include(":common:ui") include(":common:ui-charts") +include(":common:ui-markets") // endregion // region Libs modules From 88ab462aa7d13f5c6d1b6f5b5612aef6d0d2abab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 15:20:29 +0200 Subject: [PATCH 070/130] Updated on 2026-08-14 --- detekt_baseline_report.txt | 15 +- features/swap/data/detekt-baseline-debug.xml | 21 -- .../feature/swap/DefaultSwapRepository.kt | 18 +- .../swap/DefaultSwapTransactionRepository.kt | 46 ++-- .../swap/converters/ExpressDataConverter.kt | 6 +- .../swap/domain/detekt-baseline-debug.xml | 31 --- .../feature/swap/domain/SwapInteractorImpl.kt | 172 +++++++++------ features/swap/impl/detekt-baseline-debug.xml | 73 ------- .../feature/swap/analytics/StoriesEvents.kt | 2 +- .../feature/swap/analytics/SwapEvents.kt | 10 +- .../converters/AccountTokenItemConverter.kt | 14 +- .../swap/converters/TokensDataConverter.kt | 4 +- .../swap/converters/TokensDataConverterV2.kt | 2 +- .../tangem/feature/swap/model/SwapModel.kt | 200 ++++++++++-------- .../swap/model/SwapNotificationsFactory.kt | 7 +- .../swap/models/SwapSelectTokenStateHolder.kt | 4 +- .../feature/swap/models/SwapStateHolder.kt | 4 +- .../swap/models/SwapSuccessStateHolder.kt | 2 +- .../swap/preview/SwapSuccessStatePreview.kt | 2 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 13 +- .../tangem/feature/swap/ui/ProviderItem.kt | 28 +-- .../tangem/feature/swap/ui/StateBuilder.kt | 77 +++---- .../feature/swap/ui/SwapScreenContent.kt | 6 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 12 +- .../feature/swap/ui/SwapSuccessScreen.kt | 6 +- .../tangem/feature/swap/ui/TransactionCard.kt | 18 +- 26 files changed, 365 insertions(+), 428 deletions(-) delete mode 100644 features/swap/data/detekt-baseline-debug.xml delete mode 100644 features/swap/domain/detekt-baseline-debug.xml delete mode 100644 features/swap/impl/detekt-baseline-debug.xml diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index a45e2bff61..d36313f946 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,7 +1,7 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-12 13:17:20 +Date: 2025-12-13 00:34:09 Step 1: Running detekt to check for new issues... @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1378 - Modules with Issues: 72 - Average Issues per Module: 19 + Total Issues: 1271 + Modules with Issues: 69 + Average Issues per Module: 18 Progress: - Fixed: 555 out of 1933 (28%) - Remaining: 1378 + Fixed: 662 out of 1933 (34%) + Remaining: 1271 ========================================== All Modules with Issues (sorted by count) @@ -34,7 +34,6 @@ Module Issues features/markets/impl 147 features/wallet/impl 144 features/onboarding-v2/impl 128 -features/swap/impl 67 data/wallet-connect 54 features/hot-wallet/impl 51 features/walletconnect/impl 47 @@ -45,7 +44,6 @@ domain/wallets 37 features/nft/impl 34 features/tester/impl 28 core/ui 26 -features/swap/domain 25 domain/tokens 25 common/ui 24 features/yield-supply/impl 21 @@ -54,7 +52,6 @@ domain/models 21 data/visa 21 data/nft 19 core/pagination 16 -features/swap/data 15 domain/staking/models 13 data/wallets 13 features/token-recieve/impl 11 diff --git a/features/swap/data/detekt-baseline-debug.xml b/features/swap/data/detekt-baseline-debug.xml deleted file mode 100644 index 640fd2fbc1..0000000000 --- a/features/swap/data/detekt-baseline-debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository$val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value - BooleanPropertyNaming:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository$val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value - MultilineLambdaItParameter:DefaultSwapRepository.kt$DefaultSwapRepository${ Timber.e("getExchangeStatus error: $it") raise(UnknownError(it.message)) } - MultilineLambdaItParameter:DefaultSwapRepository.kt$DefaultSwapRepository${ val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) isAvailableForSwap } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ converter.convertBack( value = it, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ converter.convertBack( value = it, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ storeTransactionState( txId = transaction.txId, status = it, accountWithCurrency = null, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ val isUserWallet = it.userWalletId == userWallet.walletId.stringValue val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value isUserWallet && fromCurrency } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ val isUserWallet = it.userWalletId == userWallet.walletId.stringValue val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value isUserWallet && toCurrency } - MultilineLambdaItParameter:ExpressDataConverter.kt$ExpressDataConverter${ if (it == "0") { BigDecimal.ZERO } else { requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.swapTxTypeDTO == SwapTxTypeDTO.Swap } - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.txId == txId } - UseOrEmpty:DefaultSwapRepository.kt$DefaultSwapRepository$ex.errorBody ?: "" - UseOrEmpty:DefaultSwapRepository.kt$DefaultSwapRepository$exception.errorBody ?: "" - - diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 2f34e89a84..2b464a5d59 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -118,7 +118,7 @@ internal class DefaultSwapRepository( ) } catch (exception: Exception) { if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) } else { throw exception } @@ -138,12 +138,12 @@ internal class DefaultSwapRepository( network = initialCurrency.network, ) val currenciesList = currencyList - .filter { - val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) + .filter { currency -> + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) isAvailableForSwap } - .map { leastTokenInfoConverter.convert(it) } + .map { currency -> leastTokenInfoConverter.convert(currency) } val pairsDeferred = async { getPairsInternal( @@ -174,7 +174,7 @@ internal class DefaultSwapRepository( ) } catch (exception: Exception) { if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) } else { throw exception } @@ -221,9 +221,9 @@ internal class DefaultSwapRepository( .getOrThrow(), ) }, - catch = { - Timber.e("getExchangeStatus error: $it") - raise(UnknownError(it.message)) + catch = { exception -> + Timber.e("getExchangeStatus error: $exception") + raise(UnknownError(exception.message)) }, ) } @@ -420,7 +420,7 @@ internal class DefaultSwapRepository( private fun getDataError(ex: Exception): ExpressDataError { return if (ex is ApiResponseError.HttpException) { - errorsDataConverter.convert(ex.errorBody ?: "") + errorsDataConverter.convert(ex.errorBody.orEmpty()) } else { ExpressDataError.UnknownError } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index ab4f58a5df..fb7b9d0ed1 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -51,10 +51,10 @@ internal class DefaultSwapTransactionRepository( toAccount: Account.CryptoPortfolio?, transaction: SavedSwapTransactionModel, ) { - transaction.status?.let { + transaction.status?.let { status -> storeTransactionState( txId = transaction.txId, - status = it, + status = status, accountWithCurrency = null, ) } @@ -63,8 +63,8 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ) val tokenTransactions = savedTransactions - ?.firstOrNull { - it.checkId( + ?.firstOrNull { savedTx -> + savedTx.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, @@ -73,7 +73,7 @@ internal class DefaultSwapTransactionRepository( ?.transactions ?.addOrReplace( item = transaction, - predicate = { it.txId == transaction.txId }, + predicate = { tx -> tx.txId == transaction.txId }, ) ?: listOf(transaction) mutablePreferences.setObject( @@ -117,31 +117,31 @@ internal class DefaultSwapTransactionRepository( }, ) { savedTransactions, txStatuses, accountList -> - val currencyToTxs = savedTransactions?.filter { - val isUserWallet = it.userWalletId == userWallet.walletId.stringValue - val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value - isUserWallet && toCurrency + val currencyToTxs = savedTransactions?.filter { savedTx -> + val isUserWallet = savedTx.userWalletId == userWallet.walletId.stringValue + val isToCurrency = savedTx.toCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && isToCurrency } - val currencyFromTxs = savedTransactions?.filter { - val isUserWallet = it.userWalletId == userWallet.walletId.stringValue - val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value - isUserWallet && fromCurrency + val currencyFromTxs = savedTransactions?.filter { savedTx -> + val isUserWallet = savedTx.userWalletId == userWallet.walletId.stringValue + val isFromCurrency = savedTx.fromCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && isFromCurrency } - val toTxs = currencyToTxs?.mapNotNull { + val toTxs = currencyToTxs?.mapNotNull { savedTx -> converter.convertBack( - value = it, + value = savedTx, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, - onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, + onFilter = { tx -> tx.swapTxTypeDTO == SwapTxTypeDTO.Swap }, ) }.orEmpty() - val fromTxs = currencyFromTxs?.mapNotNull { + val fromTxs = currencyFromTxs?.mapNotNull { savedTx -> converter.convertBack( - value = it, + value = savedTx, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, @@ -161,9 +161,9 @@ internal class DefaultSwapTransactionRepository( ) val tokenTransactions = savedList ?.asSequence() - ?.map { - it.copy(transactions = it.transactions.filterNot { it.txId == txId }) - }?.filterNot { it.transactions.isEmpty() } + ?.map { savedTx -> + savedTx.copy(transactions = savedTx.transactions.filterNot { tx -> tx.txId == txId }) + }?.filterNot { savedTx -> savedTx.transactions.isEmpty() } ?.toList() if (tokenTransactions.isNullOrEmpty()) { @@ -261,8 +261,8 @@ internal class DefaultSwapTransactionRepository( toAccount = toAccount, tokenTransactions = transactions, ), - predicate = { - it.checkId( + predicate = { savedTx -> + savedTx.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 34c663840b..8d605d8dfd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -25,11 +25,11 @@ internal class ExpressDataConverter : Converter + if (feeValue == "0") { BigDecimal.ZERO } else { - requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } + requireNotNull(feeValue.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } ExpressTransactionModel.DEX( diff --git a/features/swap/domain/detekt-baseline-debug.xml b/features/swap/domain/detekt-baseline-debug.xml deleted file mode 100644 index e1d3527ea0..0000000000 --- a/features/swap/domain/detekt-baseline-debug.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val allQuotesFound = cachedQuotes?.all { it.value !is QuoteStatus.Empty } == true - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val notCustomTokenFilter = !it.currency.isCustom - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - CanBeNonNullable:SwapInteractorImpl.kt$SwapInteractorImpl$spenderAddress: String? - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$fromToken.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$toToken.value.networkAddress?.defaultAddress?.value.orEmpty() - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create approveTransaction") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create swap CEX tx data") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && it.network.derivationPath == fromTokenStatus.currency.network.derivationPath } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && tokenInfoForFilter(it).network == currency.network.backendId } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount val notCustomTokenFilter = !it.currency.isCustom statusFilter && currencyFilter && notCustomTokenFilter } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ val listTokenInfo = tokenInfoForAvailable(it) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && isAvailableForSwap ) { it.providers } else { null } } - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$getCoinBalanceAfterTransaction(fromTokenStatus, amount, includeFeeInAmount, fee) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$isAllowedToSpend(networkId, fromToken.currency, amount, it) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) - NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl$account - NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl${ it.isAvailable } - NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$${e.message} - SuspendFunSwallowedCancellation:SwapInteractorImpl.kt$SwapInteractorImpl$runCatching - - diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index aa8415a3c9..fc9b3e26de 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -43,6 +43,7 @@ import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -122,12 +123,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses - .filter { - val currencyFilter = it.currency.network.backendId != currency.network.backendId || - it.currency.getContractAddress() != currency.getContractAddress() - val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - val notCustomTokenFilter = !it.currency.isCustom - statusFilter && currencyFilter && notCustomTokenFilter + .filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom + hasValidStatus && isDifferentCurrency && isNotCustomToken } if (walletCurrencyStatusesExceptInitial.isEmpty()) { @@ -169,15 +171,15 @@ internal class SwapInteractorImpl @AssistedInject constructor( val walletAccountCurrencyStatusesExceptInitial = walletAccountCurrencyStatuses .mapNotNull { accountStatus -> - val filteredCurrencies = accountStatus.flattenCurrencies().filter { - val currencyFilter = it.currency.network.backendId != currency.network.backendId || - it.currency.getContractAddress() != currency.getContractAddress() + val filteredCurrencies = accountStatus.flattenCurrencies().filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() - val statusFilter = - it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - val notCustomTokenFilter = !it.currency.isCustom + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom - statusFilter && currencyFilter && notCustomTokenFilter + hasValidStatus && isDifferentCurrency && isNotCustomToken } if (filteredCurrencies.isNotEmpty()) { @@ -228,9 +230,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { - tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(it).network == currency.network.backendId + val filteredPairs = leastPairs.filter { pair -> + tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && + tokenInfoForFilter(pair).network == currency.network.backendId } val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { cryptoCurrencyStatus -> @@ -261,16 +263,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { - tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(it).network == currency.network.backendId + val filteredPairs = leastPairs.filter { pair -> + tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && + tokenInfoForFilter(pair).network == currency.network.backendId } - val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (account, currencyStatusList) -> - val account = account as? Account.CryptoPortfolio ?: return@mapNotNull null + val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (accountEntry, currencyStatusList) -> + val cryptoPortfolio = accountEntry as? Account.CryptoPortfolio ?: return@mapNotNull null AccountSwapAvailability( - account = account, + account = cryptoPortfolio, currencyList = currencyStatusList.map { currencyStatus -> val providers = findProvidersForPair( cryptoCurrencyStatuses = currencyStatus, @@ -280,7 +282,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val isUnavailable = providers.isNullOrEmpty() AccountSwapCurrency( isAvailable = !isUnavailable, - account = account, + account = cryptoPortfolio, cryptoCurrencyStatus = currencyStatus, providers = providers.orEmpty(), ) @@ -304,13 +306,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( val requirements = getAssetRequirementsUseCase.invoke(userWalletId, cryptoCurrencyStatuses.currency).getOrNull() val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) - return swapPairsLeastList.firstNotNullOfOrNull { - val listTokenInfo = tokenInfoForAvailable(it) + return swapPairsLeastList.firstNotNullOfOrNull { pair -> + val listTokenInfo = tokenInfoForAvailable(pair) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && isAvailableForSwap ) { - it.providers + pair.providers } else { null } @@ -351,8 +353,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount?.value, contractAddress = permissionOptions.forTokenContractAddress, spenderAddress = permissionOptions.spenderAddress, - ).getOrElse { - Timber.e(it, "Failed to create approveTransaction") + ).getOrElse { error -> + Timber.e(error, "Failed to create approveTransaction") return SwapTransactionState.Error.UnknownError } @@ -450,6 +452,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( }.toMap() } + @Suppress("LongMethod") private suspend fun manageDex( networkId: String, fromToken: CryptoCurrencyStatus, @@ -487,8 +490,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = maybeQuotes.fold( ifRight = { quotes -> - quotes.allowanceContract?.let { - isAllowedToSpend(networkId, fromToken.currency, amount, it) + quotes.allowanceContract?.let { allowanceContract -> + isAllowedToSpend( + networkId = networkId, + fromToken = fromToken.currency, + amount = amount, + spenderAddress = allowanceContract, + ) } != false }, ifLeft = { false }, @@ -626,7 +634,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( is TxFeeState.MultipleFeeState -> feeState.getFeeByType(selectedFee).feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val balanceAfterTransaction = getCoinBalanceAfterTransaction(fromTokenStatus, amount, includeFeeInAmount, fee) + val balanceAfterTransaction = getCoinBalanceAfterTransaction( + fromTokenStatus = fromTokenStatus, + amount = amount, + includeFeeInAmount = includeFeeInAmount, + fee = fee, + ) val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { includeFeeInAmount.amountSubtractFee } else { @@ -866,8 +879,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWalletId = userWalletId, network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()), - ).getOrElse { - Timber.e(it, "Failed to create swap dex tx data") + ).getOrElse { error -> + Timber.e(error, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } return handleSwapResult( @@ -938,11 +951,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) return result.fold( ifRight = { txHash -> + val networkAddress = currencyToSendStatus.value.networkAddress + val fromAddress = networkAddress?.defaultAddress?.value.orEmpty() repository.exchangeSent( userWallet = userWallet, txId = swapData.transaction.txId, fromNetwork = currencyToSendStatus.currency.network.backendId, - fromAddress = currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromAddress, payInAddress = payInAddress, txHash = txHash, payInExtraId = swapData.transaction.txExtraId, @@ -1000,12 +1015,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( expressOperationType: ExpressOperationType, isTangemPayWithdrawal: Boolean, ): SwapTransactionState { + val fromNetworkAddress = currencyToSend.value.networkAddress + val fromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() + val toNetworkAddress = currencyToGet.value.networkAddress + val toAddress = toNetworkAddress?.defaultAddress?.value.orEmpty() val exchangeData = repository.getExchangeData( userWallet = userWallet, fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, toContractAddress = currencyToGet.currency.getContractAddress(), - fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromAddress, toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, @@ -1013,10 +1032,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, expressOperationType = expressOperationType, - toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(), + toAddress = toAddress, refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null, - ).getOrElse { return SwapTransactionState.Error.ExpressError(it) } + ).getOrElse { error -> return SwapTransactionState.Error.ExpressError(error) } val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError @@ -1063,8 +1082,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, - ).getOrElse { - Timber.e(it, "Failed to create swap CEX tx data") + ).getOrElse { error -> + Timber.e(error, "Failed to create swap CEX tx data") return SwapTransactionState.Error.UnknownError } @@ -1079,14 +1098,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) val derivationPath = currencyToSend.currency.network.derivationPath.value + val cexNetworkAddress = currencyToSend.value.networkAddress + val cexFromAddress = cexNetworkAddress?.defaultAddress?.value.orEmpty() return result.fold( - ifLeft = { SwapTransactionState.Error.TransactionError(it) }, + ifLeft = { error -> SwapTransactionState.Error.TransactionError(error) }, ifRight = { txHash -> repository.exchangeSent( userWallet = userWallet, txId = exchangeDataCex.txId, fromNetwork = currencyToSend.currency.network.backendId, - fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = cexFromAddress, payInAddress = txData.destinationAddress, txHash = txHash, payInExtraId = exchangeDataCex.txExtraId, @@ -1182,8 +1203,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo: Boolean, ): CryptoCurrencyStatus? { val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo) - ?: initialToCurrencyResolver.tryGetWithMaxAmount(state, isReverseFromTo) + return initialToCurrencyResolver.tryGetFromCache( + userWallet = userWallet, + initialCryptoCurrency = initialCryptoCurrency, + state = state, + isReverseFromTo = isReverseFromTo, + ) + ?: initialToCurrencyResolver.tryGetWithMaxAmount(state = state, isReverseFromTo = isReverseFromTo) ?: group.available.firstOrNull()?.currencyStatus } @@ -1193,9 +1219,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo: Boolean, ): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) - ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state, isReverseFromTo) - ?: group.accountCurrencyList.firstNotNullOfOrNull { it.currencyList.firstOrNull { it.isAvailable } } + return initialToCurrencyResolver.tryGetFromCacheV2( + userWallet = userWallet, + initialCryptoCurrency = initialCryptoCurrency, + state = state, + isReverseFromTo = isReverseFromTo, + ) + ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state = state, isReverseFromTo = isReverseFromTo) + ?: group.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.isAvailable + } + } } override fun getNativeToken(networkId: String): CryptoCurrency { @@ -1367,7 +1402,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - spenderAddress = quoteModel.allowanceContract, + spenderAddress = requireNotNull(quoteModel.allowanceContract) { + "allowanceContract is required for DEX" + }, ) if (state !is SwapState.QuotesLoadedState) return state state.copy( @@ -1548,19 +1585,23 @@ internal class SwapInteractorImpl @AssistedInject constructor( selectedFee: FeeType, expressOperationType: ExpressOperationType, ): SwapState { + val fromNetworkAddress = fromToken.value.networkAddress + val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() + val toNetworkAddress = toToken.value.networkAddress + val dexToAddress = toNetworkAddress?.defaultAddress?.value.orEmpty() return repository.getExchangeData( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), - fromAddress = fromToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = dexFromAddress, toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + toAddress = dexToAddress, refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, expressOperationType = expressOperationType, ).fold( @@ -1813,7 +1854,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromAccount: Account.CryptoPortfolio?, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, - spenderAddress: String?, + spenderAddress: String, isAllowedToSpend: Boolean, ): SwapState { val fromToken = fromTokenStatus.currency @@ -1837,7 +1878,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val derivationPath = fromToken.network.derivationPath.value // setting up amount for approve with given amount for swap [SwapApproveType.Limited] val callData = SmartContractCallDataProviderFactory.getApprovalCallData( - spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, + spenderAddress = spenderAddress, amount = swapAmount.value.convertToSdkAmount(fromTokenStatus), blockchain = fromToken.network.toBlockchain(), ) @@ -2212,9 +2253,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( val token = tokens .filterIsInstance() - .find { - it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && - it.network.derivationPath == fromTokenStatus.currency.network.derivationPath + .find { cryptoToken -> + cryptoToken.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && + cryptoToken.network.derivationPath == fromTokenStatus.currency.network.derivationPath } SwapFeeState.NotEnough( @@ -2268,18 +2309,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun Set.getQuotesOrEmpty(): Set { - return runCatching { - val cachedQuotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) + return runSuspendCatching { + val cachedQuotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty) - val allQuotesFound = cachedQuotes?.all { it.value !is QuoteStatus.Empty } == true + val areAllQuotesFound = cachedQuotes?.all { quote -> quote.value !is QuoteStatus.Empty } == true - if (allQuotesFound) return@runCatching cachedQuotes + if (areAllQuotesFound) return@runSuspendCatching cachedQuotes.orEmpty() val currenciesIds = if (cachedQuotes.isNullOrEmpty()) { - this + this@getQuotesOrEmpty } else { - cachedQuotes.mapNotNullTo(hashSetOf()) { - if (it.value is QuoteStatus.Empty) it.rawCurrencyId else null + cachedQuotes.mapNotNullTo(hashSetOf()) { quote -> + if (quote.value is QuoteStatus.Empty) quote.rawCurrencyId else null } } @@ -2287,10 +2328,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), ) - quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) + quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty).orEmpty() + }.getOrElse { e -> + Timber.e(e, "Failed to get quotes: ${e.message.orEmpty()}") + emptySet() } - .getOrNull() - .orEmpty() } private fun isSolana(networkId: String): Boolean { @@ -2302,7 +2344,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message}") + Timber.e("Failed to format the hash: ${e.message.orEmpty()}") hash } } diff --git a/features/swap/impl/detekt-baseline-debug.xml b/features/swap/impl/detekt-baseline-debug.xml deleted file mode 100644 index 72441faba2..0000000000 --- a/features/swap/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - BooleanPropertyNaming:ChooseFeeBottomSheet.kt$val showDivider = content.feeItems.lastIndex != index - BooleanPropertyNaming:SwapEvents.kt$SwapEvents.ChooseTokenScreenOpened$val availableTokens: Boolean - BooleanPropertyNaming:SwapEvents.kt$SwapEvents.ChooseTokenScreenResult$val tokenChosen: Boolean - BooleanPropertyNaming:SwapNotificationsFactory.kt$SwapNotificationsFactory$val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken - BooleanPropertyNaming:SwapSelectTokenStateHolder.kt$SwapSelectTokenStateHolder$val afterSearch: Boolean - BooleanPropertyNaming:SwapSelectTokenStateHolder.kt$TokenToSelectState.TokenToSelect$val available: Boolean = true - BooleanPropertyNaming:SwapStateHolder.kt$SwapButton$val enabled: Boolean - BooleanPropertyNaming:SwapStateHolder.kt$TransactionCardType.ReadOnly$val showWarning: Boolean = false - BooleanPropertyNaming:SwapSuccessStateHolder.kt$SwapSuccessStateHolder$val showStatusButton: Boolean - CastNullableToNonNullableType:SwapModel.kt$SwapModel$as - MaxChainedCallsOnSameLine:SwapModel.kt$SwapModel$it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals - MaxChainedCallsOnSameLine:SwapNotificationsFactory.kt$SwapNotificationsFactory$quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ TokenItemState.TitleState.Content( text = stringReference(value = it.currency.name), isAvailable = false, ) } - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ createSubtitleState( status = it, isAvailable = false, text = unavailableErrorText, ) } - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ createSubtitleState( status = it, isAvailable = true, text = stringReference(value = it.currency.symbol), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = if (it > 0) "+$it%" else "$it%", style = TangemTheme.typography.body2, color = textColor, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), overflow = TextOverflow.Ellipsis, maxLines = 1, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, maxLines = 1, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ LegalState( title = resourceReference(R.string.common_privacy_policy), link = it, onClick = actions.onLinkClick, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ LegalState( title = resourceReference(R.string.common_terms_of_use), link = it, onClick = actions.onLinkClick, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ it is SwapNotificationUM.Error || it is NotificationUM.Error || it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || it is SwapNotificationUM.Warning.NeedReserveToCreateAccount || it is SwapNotificationUM.Info.PermissionNeeded } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ it.convertToProviderBottomSheetState( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ val selectedItem = when (it) { FeeType.NORMAL -> txFeeState.normalFee FeeType.PRIORITY -> txFeeState.priorityFee } actions.onSelectFeeType.invoke(selectedItem) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ val tokenInfo = tokenSwapInfoForProviders[it.id] if (it is ProviderState.Content && tokenInfo != null) { val rateString = tokenInfo.tokenAmount .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) it.copy( subtitle = stringReference(rateString), percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> PercentDifference.Value(percent) } ?: PercentDifference.Value(0f), ) } else { it } } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ when (it) { is TokenToSelectState.TokenToSelect -> { it.copy( addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), ) } is TokenToSelectState.Title -> { it } } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ AccountCryptoCurrencyStatus( account = it.account, status = it.cryptoCurrencyStatus, ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.d("${coin.id} balance is ${it.value.amount}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, cryptoCurrencyStatus = it, ).getOrNull() ?: it, ) uiState = if (isFromCurrency) { dataState = dataState.copy(fromCryptoCurrency = it) stateBuilder.updateSendCurrencyBalance(uiState, it) } else { dataState = dataState.copy(toCryptoCurrency = it) stateBuilder.updateReceiveCurrencyBalance(uiState, it) } startLoadingQuotesFromLastState(isSilent = true) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, selectedCurrency = null, selectedAccount = null, isReverseFromTo = isReverseFromTo, ) uiState = stateBuilder.createInitialErrorState( uiState, (it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, ) { uiState = stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, initialCurrencyTo = initialCurrencyTo, fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(), ) initTokens(isReverseFromTo) } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it) startLoadingQuotesFromLastState() makeDefaultAlert() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it.message.orEmpty()) makeDefaultAlert() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, currency = it, ).getOrNull() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, cryptoCurrencyId = it.id, ).getOrNull() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) { it.value.fromTokenInfo.amountFiat.divide( it.value.toTokenInfo.amountFiat, it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, RoundingMode.HALF_UP, ) } else { BigDecimal.ZERO } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ if (it.key != selectedProviderEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( selectedProviderRate.divide(amount, RoundingMode.HALF_UP), ).multiply(hundredPercent) it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() } else { null } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ isBalanceHidden = it.isBalanceHidden uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ it is SwapState.SwapError && ( it.error is ExpressDataError.ExchangeTooSmallAmountError || it.error is ExpressDataError.ExchangeTooBigAmountError ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = it, isFromCurrency = false, ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = it, isFromCurrency = true, ) } - 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: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), ) } - NamedArguments:SwapModel.kt$SwapModel$PeriodicTask( UPDATE_DELAY, task = { uiState = stateBuilder.createSilentLoadState(uiState) runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, reduceBalanceBy = reduceBalanceBy, swapDataModel = null, approveDataModel = null, ) swapInteractor.findBestQuote( fromToken = fromToken, fromAccount = fromAccount, toToken = toToken, toAccount = toAccount, providers = toProvidersList, amountToSwap = amount, reduceBalanceBy = reduceBalanceBy, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } }, onSuccess = { providersState -> if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) val successStates = providersState.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") } }, onError = { Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) - NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus } - NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus.currency.id.value == id } - NoNameShadowing:SwapModel.kt$SwapModel${ it.key == selectedSwapProvider } - NullableToStringCall:SwapModel.kt$SwapModel$${currencyStatus.value.amount} - NullableToStringCall:SwapModel.kt$SwapModel$${it.value.amount} - NullableToStringCall:TransactionCard.kt$data.toString() - PropertyUsedBeforeDeclaration:SwapModel.kt$SwapModel$isAccountsMode - SuspendFunSwallowedCancellation:SwapModel.kt$SwapModel$runCatching - UnnecessaryEventHandlerParameter:ChooseFeeBottomSheet.kt$onReadMoreClick: (String) -> Unit - UnnecessaryLet:SwapModel.kt$SwapModel$let { return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() } - UnusedImports:TransactionCard.kt$import com.tangem.domain.models.account.Account - UseEmptyCounterpart:StoriesEvents.kt$StoriesEvents$mapOf() - UseEmptyCounterpart:SwapEvents.kt$SwapEvents$mapOf() - UseOrEmpty:StateBuilder.kt$StateBuilder$initialCurrencyTo?.symbol ?: "" - UseOrEmpty:SwapModel.kt$SwapModel$receiveToken ?: "" - UseOrEmpty:SwapScreenContent.kt$swapCardState.tokenIconUrl ?: "" - VarCouldBeVal:SwapModel.kt$SwapModel$private var swapRouter: SwapRouter = SwapRouter(router = router) - - diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt index b7f89e9288..1174a84ccc 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.WATCHED sealed class StoriesEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent("Stories", event, params) { data class SwapStories( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index c1ee5ff99d..e1c2640942 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -15,7 +15,7 @@ private const val PROMO_CATEGORY = "Promo" sealed class SwapEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { data class SwapScreenOpened(val token: String) : SwapEvents( @@ -25,15 +25,15 @@ sealed class SwapEvents( class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( + data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", - params = mapOf("Available tokens" to if (availableTokens) "Yes" else "No"), + params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val tokenChosen: Boolean, val token: String? = null) : SwapEvents( + data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { - put("Token Chosen", if (tokenChosen) "Yes" else "No") + put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } }, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index f556cdf5e5..4315d780ac 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -45,11 +45,11 @@ internal class AccountTokenItemConverter( fun createAvailableItemConverter(): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, - subtitleStateProvider = { + subtitleStateProvider = { status -> createSubtitleState( - status = it, + status = status, isAvailable = true, - text = stringReference(value = it.currency.symbol), + text = stringReference(value = status.currency.symbol), ) }, subtitle2StateProvider = ::createSubtitle2State, @@ -64,15 +64,15 @@ internal class AccountTokenItemConverter( return TokenItemStateConverter( appCurrency = appCurrency, iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, - titleStateProvider = { + titleStateProvider = { status -> TokenItemState.TitleState.Content( - text = stringReference(value = it.currency.name), + text = stringReference(value = status.currency.name), isAvailable = false, ) }, - subtitleStateProvider = { + subtitleStateProvider = { status -> createSubtitleState( - status = it, + status = status, isAvailable = false, text = unavailableErrorText, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index e9f9ec7e68..ba936978ec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -58,7 +58,7 @@ internal class TokensDataConverter( onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, isBalanceHidden = isBalanceHiddenProvider(), - afterSearch = group.isAfterSearch, + isAfterSearch = group.isAfterSearch, ) } @@ -71,7 +71,7 @@ internal class TokensDataConverter( id = cryptoCurrencyStatus.currency.id.value, name = cryptoCurrencyStatus.currency.name, symbol = cryptoCurrencyStatus.currency.symbol, - available = isAvailable, + isAvailable = isAvailable, tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( amount = formatCryptoAmount(cryptoCurrencyStatus), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index 3dbd07cda3..90dd691aaa 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -60,7 +60,7 @@ internal class TokensDataConverterV2( onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, isBalanceHidden = isBalanceHidden, - afterSearch = tokensDataState.isAfterSearch, + isAfterSearch = tokensDataState.isAfterSearch, ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d5884d3b72..4cdd1f15ff 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -139,6 +139,7 @@ internal class SwapModel @Inject constructor( private var initialToStatus: CryptoCurrencyStatus? = null private var isBalanceHidden = true + private var isAccountsMode: Boolean = false private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -151,7 +152,10 @@ internal class SwapModel @Inject constructor( ) private val inputNumberFormatter = - InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat) + InputNumberFormatter( + NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat + ?: error("NumberFormat is not DecimalFormat"), + ) private val amountDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() @@ -170,18 +174,17 @@ internal class SwapModel @Inject constructor( private var isOrderReversed = false private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) - private var swapRouter: SwapRouter = SwapRouter(router = router) + private val swapRouter: SwapRouter = SwapRouter(router = router) private var userCountry: UserCountry? = null private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null - private var isAccountsMode: Boolean = false - private val isUserResolvableError: (SwapState) -> Boolean = { - it is SwapState.SwapError && + private val isUserResolvableError: (SwapState) -> Boolean = { swapState -> + swapState is SwapState.SwapError && ( - it.error is ExpressDataError.ExchangeTooSmallAmountError || - it.error is ExpressDataError.ExchangeTooBigAmountError + swapState.error is ExpressDataError.ExchangeTooSmallAmountError || + swapState.error is ExpressDataError.ExchangeTooBigAmountError ) } @@ -209,10 +212,10 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currency = initialCurrencyFrom, ).getOrNull() - val toAccountStatus = initialCurrencyTo?.let { + val toAccountStatus = initialCurrencyTo?.let { currencyTo -> getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, - currency = it, + currency = currencyTo, ).getOrNull() } @@ -227,10 +230,10 @@ internal class SwapModel @Inject constructor( } } else { val fromStatus = getFromStatus() - val toStatus = initialCurrencyTo?.let { + val toStatus = initialCurrencyTo?.let { currencyTo -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, - cryptoCurrencyId = it.id, + cryptoCurrencyId = currencyTo.id, ).getOrNull() } @@ -247,8 +250,8 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) getBalanceHidingSettingsUseCase() - .onEach { - isBalanceHidden = it.isBalanceHidden + .onEach { settings -> + isBalanceHidden = settings.isBalanceHidden uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } .launchIn(modelScope) @@ -274,7 +277,7 @@ internal class SwapModel @Inject constructor( val isAnyAvailableAccountTokensFrom = !dataState.tokensDataState?.fromGroup?.accountCurrencyList.isNullOrEmpty() val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom || isAnyAvailableAccountTokensTo || isAnyAvailableAccountTokensFrom - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens)) + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } private fun initTokens(isReverseFromTo: Boolean) { @@ -289,10 +292,10 @@ internal class SwapModel @Inject constructor( initialCryptoCurrency = initialCurrencyFrom, state = state, isReverseFromTo = isReverseFromTo, - )?.let { + )?.let { accountSwapCurrency -> AccountCryptoCurrencyStatus( - account = it.account, - status = it.cryptoCurrencyStatus, + account = accountSwapCurrency.account, + status = accountSwapCurrency.cryptoCurrencyStatus, ) } selectedAccountCurrency?.status to selectedAccountCurrency?.account @@ -312,23 +315,23 @@ internal class SwapModel @Inject constructor( isReverseFromTo = isReverseFromTo, ) - (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin -> subscribeToCoinBalanceUpdates( userWalletId = userWalletId, - coin = it, + coin = coin, isFromCurrency = true, ) } - (dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + (dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin -> subscribeToCoinBalanceUpdates( userWalletId = userWalletId, - coin = it, + coin = coin, isFromCurrency = false, ) } - }.onFailure { - Timber.e(it) + }.onFailure { error -> + Timber.e(error) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, @@ -339,7 +342,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.createInitialErrorState( uiState, - (it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, + (error as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, ) { uiState = stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, @@ -496,7 +499,7 @@ internal class SwapModel @Inject constructor( toProvidersList: List, ): PeriodicTask> { return PeriodicTask( - UPDATE_DELAY, + delay = UPDATE_DELAY, task = { uiState = stateBuilder.createSilentLoadState(uiState) runCatching(dispatchers.io) { @@ -534,8 +537,8 @@ internal class SwapModel @Inject constructor( Timber.e("Accidentally empty quotes list") } }, - onError = { - Timber.e("Error when loading quotes: $it") + onError = { error -> + Timber.e("Error when loading quotes: $error") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) @@ -599,13 +602,13 @@ internal class SwapModel @Inject constructor( } private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { - val receiveToken = dataState.toCryptoCurrency?.currency?.let { - "${it.network.backendId}:${it.symbol}" + val receiveToken = dataState.toCryptoCurrency?.currency?.let { currency -> + "${currency.network.backendId}:${currency.symbol}" } analyticsErrorEventHandler.sendErrorEvent( SwapEvents.NoticeProviderError( sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", - receiveToken = receiveToken ?: "", + receiveToken = receiveToken.orEmpty(), provider = provider, errorCode = error.code, errorMessage = error.message, @@ -614,7 +617,7 @@ internal class SwapModel @Inject constructor( } private fun updateLoadedQuotes(state: Map): Pair { - val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } + val nonEmptyStates = state.filter { entry -> entry.value !is SwapState.EmptyAmountState } val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { selectProvider(state) } else { @@ -624,8 +627,8 @@ internal class SwapModel @Inject constructor( selectedProvider = selectedSwapProvider, lastLoadedSwapStates = state, ) - selectedSwapProvider?.let { - return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() + if (selectedSwapProvider != null) { + return nonEmptyStates.entries.first { entry -> entry.key == selectedSwapProvider }.toPair() } return state.entries.first().toPair() } @@ -790,8 +793,8 @@ internal class SwapModel @Inject constructor( processTangemPayWithdrawal(swapTransactionState = swapTransactionState) } } - }.onFailure { - Timber.e(it) + }.onFailure { error -> + Timber.e(error) startLoadingQuotesFromLastState() makeDefaultAlert() } @@ -867,7 +870,7 @@ internal class SwapModel @Inject constructor( @Suppress("LongMethod") private fun givePermissionsToSwap() { modelScope.launch(dispatchers.main) { - runCatching { + runSuspendCatching { val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) { "dataState.fromCryptoCurrency might not be null" } @@ -884,7 +887,7 @@ internal class SwapModel @Inject constructor( TxFeeState.Empty -> { makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") - return@runCatching + return@launch } is TxFeeState.MultipleFeeState -> fee.priorityFee is TxFeeState.SingleFeeState -> fee.fee @@ -931,8 +934,8 @@ internal class SwapModel @Inject constructor( } } }.onFailure { makeDefaultAlert() } - }.onFailure { - Timber.e(it.message.orEmpty()) + }.onFailure { error -> + Timber.e(error.message.orEmpty()) makeDefaultAlert() } } @@ -947,13 +950,13 @@ internal class SwapModel @Inject constructor( tokenDataState.toGroup } - val available = group.available.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + val available = group.available.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } - val unavailable = group.unavailable.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + val unavailable = group.unavailable.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> @@ -998,8 +1001,8 @@ internal class SwapModel @Inject constructor( val tokens = dataState.tokensDataState ?: return val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) - foundToken?.currency?.symbol?.let { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) + foundToken?.currency?.symbol?.let { symbol -> + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) } if (foundToken != null) { @@ -1079,8 +1082,10 @@ internal class SwapModel @Inject constructor( tokens.fromGroup } else { tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { - it.currencyList.firstOrNull { it.cryptoCurrencyStatus.currency.id.value == id } + }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + } } accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account } else { @@ -1088,7 +1093,9 @@ internal class SwapModel @Inject constructor( tokens.fromGroup } else { tokens.toGroup - }.available.firstOrNull { it.currencyStatus.currency.id.value == id }?.currencyStatus to null + }.available.firstOrNull { swapAvailability -> + swapAvailability.currencyStatus.currency.id.value == id + }?.currencyStatus to null } } @@ -1105,7 +1112,7 @@ internal class SwapModel @Inject constructor( currency = coin, ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount}") + Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( @@ -1135,24 +1142,24 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currencyId = coin.id, isSingleWalletWithTokens = false, - ).mapNotNull { (it as? Either.Right)?.value } + ).mapNotNull { either -> (either as? Either.Right)?.value } .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes - .onEach { - Timber.d("${coin.id} balance is ${it.value.amount}") + .onEach { status -> + Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, - cryptoCurrencyStatus = it, - ).getOrNull() ?: it, + cryptoCurrencyStatus = status, + ).getOrNull() ?: status, ) uiState = if (isFromCurrency) { - dataState = dataState.copy(fromCryptoCurrency = it) - stateBuilder.updateSendCurrencyBalance(uiState, it) + dataState = dataState.copy(fromCryptoCurrency = status) + stateBuilder.updateSendCurrencyBalance(uiState, status) } else { - dataState = dataState.copy(toCryptoCurrency = it) - stateBuilder.updateReceiveCurrencyBalance(uiState, it) + dataState = dataState.copy(toCryptoCurrency = status) + stateBuilder.updateReceiveCurrencyBalance(uiState, status) } startLoadingQuotesFromLastState(isSilent = true) @@ -1179,8 +1186,8 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed - dataState.tokensDataState?.let { - updateTokensState(it) + dataState.tokensDataState?.let { tokensDataState -> + updateTokensState(tokensDataState) } val minTxAmount = getMinimumTransactionAmountSyncUseCase( @@ -1259,8 +1266,8 @@ internal class SwapModel @Inject constructor( } private fun onMaxAmountClicked() { - dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(it) + dataState.fromCryptoCurrency?.let { fromCurrency -> + val balance = swapInteractor.getTokenBalance(fromCurrency) onAmountChanged(balance.formatToUIRepresentation()) } } @@ -1324,7 +1331,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) } else { if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = false)) + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) } swapRouter.back() } @@ -1358,9 +1365,9 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) } }, - onSelectFeeType = { + onSelectFeeType = { feeType -> uiState = stateBuilder.dismissBottomSheet(uiState) - dataState = dataState.copy(selectedFee = it) + dataState = dataState.copy(selectedFee = feeType) modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } @@ -1377,8 +1384,8 @@ internal class SwapModel @Inject constructor( needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, - onProviderSelect = { - val provider = findAndSelectProvider(it) + onProviderSelect = { providerId -> + val provider = findAndSelectProvider(providerId) val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { @@ -1478,11 +1485,14 @@ internal class SwapModel @Inject constructor( private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { // finding best quotes - return state.minByOrNull { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) { - it.value.fromTokenInfo.amountFiat.divide( - it.value.toTokenInfo.amountFiat, - it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, + return state.minByOrNull { entry -> + val toTokenInfo = entry.value.toTokenInfo + val fromAmountFiat = entry.value.fromTokenInfo.amountFiat + val toAmountFiat = toTokenInfo.amountFiat + if (!fromAmountFiat.isNullOrZero() && !toAmountFiat.isNullOrZero()) { + fromAmountFiat.divide( + toAmountFiat, + toTokenInfo.cryptoCurrencyStatus.currency.decimals, RoundingMode.HALF_UP, ) } else { @@ -1492,17 +1502,19 @@ internal class SwapModel @Inject constructor( } private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map { - val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull() - ?: return emptyMap() + val selectedProviderEntry = state + .filter { entry -> entry.key.providerId == selectedProviderId } + .entries + .firstOrNull() ?: return emptyMap() val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value val hundredPercent = BigDecimal("100") - return state.entries.mapNotNull { - if (it.key != selectedProviderEntry.key) { - val amount = it.value.toTokenInfo.tokenAmount.value + return state.entries.mapNotNull { entry -> + if (entry.key != selectedProviderEntry.key) { + val amount = entry.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( selectedProviderRate.divide(amount, RoundingMode.HALF_UP), ).multiply(hundredPercent) - it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() + entry.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() } else { null } @@ -1534,11 +1546,15 @@ internal class SwapModel @Inject constructor( return if (accountsFeatureToggles.isFeatureEnabled) { groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> - currencyList.find { idToFind == it.cryptoCurrencyStatus.currency.id.value && it.isAvailable } + currencyList.find { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } }?.providers } else { - groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value } - ?.providers + groupToFind.available.find { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + }?.providers } ?.filterForTangemPayWithdrawal() .orEmpty() @@ -1553,13 +1569,13 @@ internal class SwapModel @Inject constructor( } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { - return this.filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } + return this.filter { entry -> entry.value is SwapState.QuotesLoadedState } + .mapValues { entry -> entry.value as SwapState.QuotesLoadedState } } private fun Map.consideredProvidersStates(): Map { - return this.filter { - it.value is SwapState.QuotesLoadedState || isUserResolvableError(it.value) + return this.filter { entry -> + entry.value is SwapState.QuotesLoadedState || isUserResolvableError(entry.value) } } @@ -1577,10 +1593,14 @@ internal class SwapModel @Inject constructor( val chosen = if (isOrderReversed) from else to return if (accountsFeatureToggles.isFeatureEnabled) { - currenciesGroup.accountCurrencyList.flatMap { it.currencyList.map { it.cryptoCurrencyStatus } } + currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> + accountSwapAvailability.currencyList.map { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus + } + } } else { - currenciesGroup.available.map { it.currencyStatus } - }.map { it.currency }.contains(chosen.currency) + currenciesGroup.available.map { swapAvailability -> swapAvailability.currencyStatus } + }.map { currencyStatus -> currencyStatus.currency }.contains(chosen.currency) } private fun sendPermissionApproveClickedEvent() { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 9c60150644..731e036f94 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -125,9 +125,10 @@ internal class SwapNotificationsFactory( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { add(SwapNotificationUM.Error.ApprovalInProgressWarning) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { + val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency add( SwapNotificationUM.Error.TransactionInProgressWarning( - currencySymbol = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, + currencySymbol = fromCurrency.network.currencySymbol, ), ) } @@ -290,10 +291,10 @@ internal class SwapNotificationsFactory( ) { if (hideFee) return val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return - val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && + val shouldShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken - if (needShowCoverWarning) { + if (shouldShowCoverWarning) { add( SwapNotificationUM.Error.UnableToCoverFeeWarning( fromToken = fromToken, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index aab8a21eae..8d9c1bf701 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -11,7 +11,7 @@ internal data class SwapSelectTokenStateHolder( val unavailableTokens: ImmutableList, val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, - val afterSearch: Boolean, + val isAfterSearch: Boolean, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) @@ -25,7 +25,7 @@ internal sealed class TokenToSelectState { val name: String, val symbol: String, val tokenIcon: CurrencyIconState, - val available: Boolean = true, + val isAvailable: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, ) : TokenToSelectState() } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 43985f8975..48a1a717c6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -77,7 +77,7 @@ sealed class SwapCardState { data class SwapButton( @DrawableRes val walletInteractionIcon: Int?, - val enabled: Boolean, + val isEnabled: Boolean, val onClick: () -> Unit, ) @@ -94,7 +94,7 @@ sealed interface TransactionCardType { ) : TransactionCardType data class ReadOnly( - val showWarning: Boolean = false, + val shouldShowWarning: Boolean = false, val onWarningClick: (() -> Unit)? = null, override val inputError: InputError = InputError.Empty, override val accountTitleUM: AccountTitleUM? = null, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 906782071d..5156ce3e1e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -9,7 +9,7 @@ data class SwapSuccessStateHolder( val txUrl: String, val fee: TextReference, val rate: TextReference, - val showStatusButton: Boolean, + val shouldShowStatusButton: Boolean, val providerName: TextReference, val providerType: TextReference, val providerIcon: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt index 9c3b0ea54d..77c343f43b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt @@ -17,7 +17,7 @@ internal data object SwapSuccessStatePreview { fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), providerName = TextReference.Str("1inch"), providerType = TextReference.Str(ExchangeProviderType.DEX.providerName), - showStatusButton = false, + shouldShowStatusButton = false, providerIcon = "", fromTitle = AccountTitleUM.Account( prefixText = stringReference("From"), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 3946ea9358..067df047a5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -58,14 +58,13 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { } FooterBlock( readMore = content.readMore, - readMoreUrl = content.readMoreUrl, - onReadMoreClick = content.onReadMoreClick, + onReadMoreClick = { content.onReadMoreClick(content.readMoreUrl) }, ) } } @Composable -private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMoreClick: (String) -> Unit) { +private fun FooterBlock(readMore: TextReference, onReadMoreClick: () -> Unit) { val linkText = readMore.resolveReference() val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText) val linkTextPosition = fullString.length - linkText.length @@ -81,7 +80,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore val click = { i: Int -> val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1)) if (i in readMoreStyle.start..readMoreStyle.end) { - onReadMoreClick(readMoreUrl) + onReadMoreClick() } } @@ -102,7 +101,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { content.feeItems.forEachIndexed { index, feeItem -> val isSelected = feeItem.feeType == content.selectedFee - val showDivider = content.feeItems.lastIndex != index + val shouldShowDivider = content.feeItems.lastIndex != index val symbol = " ${feeItem.symbolCrypto}" val preDotText = "${feeItem.amountCrypto}$symbol" val postDot = feeItem.amountFiatFormatted @@ -117,7 +116,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, - showDivider = showDivider, + showDivider = shouldShowDivider, ) } FeeType.PRIORITY -> { @@ -129,7 +128,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, - showDivider = showDivider, + showDivider = shouldShowDivider, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index ac6ebfd1b3..a610eaac34 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -132,16 +132,16 @@ private fun ProviderContentState( modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), ) } - AnimatedContent(targetState = state.name, label = "") { + AnimatedContent(targetState = state.name, label = "") { name -> Text( - text = it, + text = name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, ) } - AnimatedContent(targetState = state.type, label = "") { + AnimatedContent(targetState = state.type, label = "") { type -> Text( - text = it, + text = type, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), @@ -162,9 +162,9 @@ private fun ProviderContentState( end = TangemTheme.dimens.spacing56, ), ) { - AnimatedContent(targetState = state.subtitle, label = "") { + AnimatedContent(targetState = state.subtitle, label = "") { subtitle -> Text( - text = it.resolveReference(), + text = subtitle.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, @@ -179,9 +179,9 @@ private fun ProviderContentState( } else { TangemTheme.colors.text.warning } - AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { + AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { percentValue -> Text( - text = if (it > 0) "+$it%" else "$it%", + text = if (percentValue > 0) "+$percentValue%" else "$percentValue%", style = TangemTheme.typography.body2, color = textColor, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), @@ -231,25 +231,25 @@ private fun ProviderUnavailableState( modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) { Row { - AnimatedContent(targetState = state.name, label = "") { + AnimatedContent(targetState = state.name, label = "") { name -> Text( - text = it, + text = name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - AnimatedContent(targetState = state.type, label = "") { + AnimatedContent(targetState = state.type, label = "") { type -> Text( - text = it, + text = type, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) } } - AnimatedContent(targetState = state.alertText, label = "") { + AnimatedContent(targetState = state.alertText, label = "") { alertText -> Text( - text = it.resolveReference(), + text = alertText.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6369135b49..64ac9aa044 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -109,7 +109,7 @@ internal class StateBuilder( type = TransactionCardType.ReadOnly(), amountEquivalent = null, tokenIconUrl = initialCurrencyTo?.iconUrl, - tokenCurrency = initialCurrencyTo?.symbol ?: "", + tokenCurrency = initialCurrencyTo?.symbol.orEmpty(), token = null, amountTextFieldValue = null, canSelectAnotherToken = false, @@ -122,7 +122,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = {}, ), onRefresh = {}, @@ -181,7 +181,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = { }, ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, @@ -246,7 +246,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = {}, ), providerState = ProviderState.Loading(), @@ -335,7 +335,7 @@ internal class StateBuilder( ), receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReadOnly( - showWarning = true, + shouldShowWarning = true, onWarningClick = actions.onReceiveCardWarningClick, accountTitleUM = getToCardAccountTitle(toAccount), ), @@ -368,7 +368,7 @@ internal class StateBuilder( fee = feeState, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = getSwapButtonEnabled(notifications), + isEnabled = getSwapButtonEnabled(notifications), onClick = actions.onSwapClick, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -397,17 +397,17 @@ internal class StateBuilder( private fun createTosState(swapProvider: SwapProvider): TosState { return TosState( - tosLink = swapProvider.termsOfUse?.let { + tosLink = swapProvider.termsOfUse?.let { termsUrl -> LegalState( title = resourceReference(R.string.common_terms_of_use), - link = it, + link = termsUrl, onClick = actions.onLinkClick, ) }, - policyLink = swapProvider.privacyPolicy?.let { + policyLink = swapProvider.privacyPolicy?.let { policyUrl -> LegalState( title = resourceReference(R.string.common_privacy_policy), - link = it, + link = policyUrl, onClick = actions.onLinkClick, ) }, @@ -420,12 +420,13 @@ internal class StateBuilder( } private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean { - return notifications.none { - it is SwapNotificationUM.Error || it is NotificationUM.Error || - it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || - it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || - it is SwapNotificationUM.Warning.NeedReserveToCreateAccount || - it is SwapNotificationUM.Info.PermissionNeeded + return notifications.none { notification -> + notification is SwapNotificationUM.Error || notification is NotificationUM.Error || + notification is SwapNotificationUM.Warning.ExpressError || + notification is SwapNotificationUM.Warning.ExpressGeneralError || + notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || + notification is SwapNotificationUM.Info.PermissionNeeded } } @@ -495,7 +496,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = actions.onSwapClick, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -592,7 +593,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = { }, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -604,7 +605,7 @@ internal class StateBuilder( fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( swapButton = uiState.swapButton.copy( - enabled = false, + isEnabled = false, ), ) } @@ -722,15 +723,17 @@ internal class StateBuilder( ) val selectTokenState = uiState.selectTokenState?.copy( isBalanceHidden = isBalanceHidden, - availableTokens = uiState.selectTokenState.availableTokens.map { - when (it) { + availableTokens = uiState.selectTokenState.availableTokens.map { tokenState -> + when (tokenState) { is TokenToSelectState.TokenToSelect -> { - it.copy( - addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), + tokenState.copy( + addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy( + isBalanceHidden = isBalanceHidden, + ), ) } is TokenToSelectState.Title -> { - it + tokenState } } }.toImmutableList(), @@ -804,7 +807,7 @@ internal class StateBuilder( fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( swapButton = uiState.swapButton.copy( - enabled = false, + isEnabled = false, ), permissionState = GiveTxPermissionState.InProgress, notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications), @@ -837,7 +840,7 @@ internal class StateBuilder( txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), - showStatusButton = shouldShowStatus, + shouldShowStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"), @@ -877,7 +880,7 @@ internal class StateBuilder( txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), - showStatusButton = false, + shouldShowStatusButton = false, providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = TextReference.EMPTY, @@ -1101,8 +1104,8 @@ internal class StateBuilder( onDismiss: () -> Unit, ): SwapStateHolder { val availableProvidersStates = providersStates.entries - .mapNotNull { - it.convertToProviderBottomSheetState( + .mapNotNull { entry -> + entry.convertToProviderBottomSheetState( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, @@ -1139,19 +1142,19 @@ internal class StateBuilder( uiState.copy( bottomSheetConfig = uiState.bottomSheetConfig.copy( content = config.copy( - providers = providers.map { - val tokenInfo = tokenSwapInfoForProviders[it.id] - if (it is ProviderState.Content && tokenInfo != null) { + providers = providers.map { providerState -> + val tokenInfo = tokenSwapInfoForProviders[providerState.id] + if (providerState is ProviderState.Content && tokenInfo != null) { val rateString = tokenInfo.tokenAmount .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) - it.copy( + providerState.copy( subtitle = stringReference(rateString), - percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> + percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent -> PercentDifference.Value(percent) } ?: PercentDifference.Value(0f), ) } else { - it + providerState } }.toImmutableList(), ), @@ -1170,8 +1173,8 @@ internal class StateBuilder( ): SwapStateHolder { val config = ChooseFeeBottomSheetConfig( selectedFee = selectedFee, - onSelectFeeType = { - val selectedItem = when (it) { + onSelectFeeType = { feeType -> + val selectedItem = when (feeType) { FeeType.NORMAL -> txFeeState.normalFee FeeType.PRIORITY -> txFeeState.priorityFee } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 486c515d86..3aa2c4d059 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -175,7 +175,7 @@ private fun TransactionCardData( balance = swapCardState.balance.orMaskWithStars(swapCardState.isBalanceHidden), textFieldValue = swapCardState.amountTextFieldValue, amountEquivalent = swapCardState.amountEquivalent, - tokenIconUrl = swapCardState.tokenIconUrl ?: "", + tokenIconUrl = swapCardState.tokenIconUrl.orEmpty(), tokenCurrency = swapCardState.tokenCurrency, priceImpact = priceImpact, networkIconRes = if (swapCardState.isNotNativeToken) swapCardState.networkIconRes else null, @@ -374,7 +374,7 @@ private fun MainButton(state: SwapStateHolder) { modifier = Modifier.fillMaxWidth(), text = stringResourceSafe(id = R.string.swapping_swap_action), iconResId = state.swapButton.walletInteractionIcon, - enabled = state.swapButton.enabled, + enabled = state.swapButton.isEnabled, onClick = state.swapButton.onClick, ) } @@ -437,7 +437,7 @@ private val state = SwapStateHolder( ), SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"), ), - swapButton = SwapButton(enabled = true, onClick = {}, walletInteractionIcon = null), + swapButton = SwapButton(isEnabled = true, onClick = {}, walletInteractionIcon = null), onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 39625f9e1f..6b0fa946f4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -57,11 +57,11 @@ internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () val modifier = Modifier.padding(padding) when { state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && - state.tokensListData.tokensList.isEmpty() && state.afterSearch -> { + state.tokensListData.tokensList.isEmpty() && state.isAfterSearch -> { TokensNotFound(modifier) } state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && - state.tokensListData.tokensList.isEmpty() && !state.afterSearch -> { + state.tokensListData.tokensList.isEmpty() && !state.isAfterSearch -> { EmptyTokensList(modifier) } else -> { @@ -313,7 +313,7 @@ private fun TokenItem( .fillMaxWidth() .height(TangemTheme.dimens.size72) .clickable( - enabled = token.available, + enabled = token.isAvailable, onClick = onTokenClick, ) .padding( @@ -336,7 +336,7 @@ private fun TokenItem( EllipsisText( text = token.name, style = TangemTheme.typography.subtitle1, - color = if (token.available) { + color = if (token.isAvailable) { TangemTheme.colors.text.primary1 } else { TangemTheme.colors.text.tertiary @@ -366,7 +366,7 @@ private fun TokenItem( maxLines = 1, softWrap = false, overflow = TextOverflow.Visible, - color = if (token.available) { + color = if (token.isAvailable) { TangemTheme.colors.text.primary1 } else { TangemTheme.colors.text.tertiary @@ -417,7 +417,7 @@ private fun TokenScreenPreview() { availableTokens = listOf(title, token, token, token).toImmutableList(), unavailableTokens = listOf(title, token, token, token).toImmutableList(), tokensListData = TokenListUMData.EmptyList, - afterSearch = false, + isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, onTokenSelected = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 97e9c3dd4d..2d51569af0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -48,7 +48,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { SwapSuccessScreenButtons( textRes = R.string.common_close, txUrl = state.txUrl, - showStatusButton = state.showStatusButton, + shouldShowStatusButton = state.shouldShowStatusButton, onExploreClick = state.onExploreButtonClick, onStatusClick = state.onStatusButtonClick, onDoneClick = onBack, @@ -160,7 +160,7 @@ private fun SwapAmountBlock( private fun SwapSuccessScreenButtons( @StringRes textRes: Int, txUrl: String, - showStatusButton: Boolean, + shouldShowStatusButton: Boolean, onExploreClick: () -> Unit, onStatusClick: () -> Unit, onDoneClick: () -> Unit, @@ -178,7 +178,7 @@ private fun SwapSuccessScreenButtons( onClick = onExploreClick, modifier = Modifier.weight(1f), ) - if (showStatusButton) { + if (shouldShowStatusButton) { SpacerW12() SecondaryButtonIconStart( text = stringResourceSafe(id = R.string.express_cex_status_button_title), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index a529440cd6..f344f04903 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -46,7 +46,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -217,9 +216,9 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie } SpacerW16() if (balance.isNotBlank()) { - AnimatedContent(targetState = balance, label = "") { + AnimatedContent(targetState = balance, label = "") { balanceText -> Text( - text = it, + text = balanceText, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier @@ -318,15 +317,15 @@ private fun Content( style = TangemTheme.typography.body2, ) } else { - AnimatedContent(targetState = amountEquivalent, label = "") { + AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = it, + text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } } - if (type.showWarning) { + if (type.shouldShowWarning) { SpacerW4() IconButton( onClick = { @@ -348,9 +347,9 @@ private fun Content( } } } else { - AnimatedContent(targetState = amountEquivalent, label = "") { + AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = it, + text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), @@ -409,6 +408,7 @@ fun Token( } } +@Suppress("NullableToStringCall") @Composable private fun TokenIcon( tokenIconUrl: String, @@ -595,7 +595,7 @@ private fun TransactionCardPreview() { private fun TransactionCardPreviewWithPriceImpact() { TransactionCard( type = TransactionCardType.ReadOnly( - showWarning = true, + shouldShowWarning = true, accountTitleUM = AccountTitleUM.Account( prefixText = resourceReference(R.string.common_from), name = AccountNameUM.DefaultMain.value, From c003dbb39ddf1e56b7618710749221d26b702b36 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 21:31:44 +0500 Subject: [PATCH 071/130] Updated on 2026-08-14 --- .../entry/WalletActivationModel.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 3f91e6d246..283d1c7eea 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -62,7 +62,7 @@ internal class WalletActivationModel @Inject constructor( val pushNotificationsCallbacks = PushNotificationsCallbacks() val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks() - val isStartingWithAccessCode = params.isBackupExists + private val isStartingWithAccessCode = params.isBackupExists val stackNavigation = StackNavigation() val startRoute = if (isStartingWithAccessCode) { WalletActivationRoute.SetAccessCode @@ -76,13 +76,23 @@ internal class WalletActivationModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() - if (startRoute is WalletActivationRoute.ManualBackupStart) { - analyticsEventHandler.send( - event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo( - source = analyticsSource.value, - action = analyticsAction.value, - ), - ) + when (startRoute) { + is WalletActivationRoute.ManualBackupStart -> { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo( + source = analyticsSource.value, + action = analyticsAction.value, + ), + ) + } + is WalletActivationRoute.SetAccessCode -> { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened( + source = analyticsSource.value, + ), + ) + } + else -> Unit } } From 9ea256e5577b8d6dd3e3e4edc9f9353916b9f46b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 12:18:53 +0500 Subject: [PATCH 072/130] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 13 +++++++-- .../res/drawable/ic_analytics_up_mini_24.xml | 13 +++++++++ domain/account/build.gradle.kts | 1 + domain/tokens/build.gradle.kts | 1 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 13 +++++++-- .../tokens/actions/BaseActionsFactory.kt | 10 +++++++ .../tokens/actions/CommonActionsFactory.kt | 7 +++++ .../actions/OutdatedDataActionsFactory.kt | 7 +++++ .../domain/tokens/model/TokenActionsState.kt | 5 ++++ .../supply/models/YieldSupplyAvailability.kt | 10 +++++++ .../YieldSupplyGetAvailabilityUseCase.kt | 26 +++++++++++++++++ features/markets/impl/build.gradle.kts | 2 ++ .../impl/analytics/PortfolioAnalyticsEvent.kt | 1 + .../impl/loader/PortfolioDataLoader.kt | 9 +++++- .../impl/model/PortfolioTokenUMConverter.kt | 9 ++++-- .../impl/model/TokenActionsHandler.kt | 29 ++++++++++++++++++- .../portfolio/impl/ui/state/QuickActionUM.kt | 9 ++++++ .../impl/ui/state/TokenActionsBSContentUM.kt | 4 +++ .../MultiWalletCurrencyActionsConverter.kt | 6 ++++ 19 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml create mode 100644 domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index b8c7e6dfc7..3301b9c21a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -1,10 +1,10 @@ package com.tangem.tap.di.domain import com.tangem.domain.blockaid.BlockAidGasEstimate -import com.tangem.domain.transaction.FeeRepository -import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository @@ -16,6 +16,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object YieldSupplyDomainModule { @@ -225,4 +226,12 @@ internal object YieldSupplyDomainModule { fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase { return YieldSupplyGetDustMinAmountUseCase() } + + @Provides + @Singleton + fun provideYieldSupplyGetAvailabilityUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyGetAvailabilityUseCase { + return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml b/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml new file mode 100644 index 0000000000..b7e8d62dff --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index da17a216f8..0d035a8fe5 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) + api(projects.domain.yieldSupply.models) implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 68bfb40179..005eba3ef1 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.promo) implementation(projects.domain.networks) implementation(projects.domain.quotes) + implementation(projects.domain.yieldSupply.models) /** Project - Api */ implementation(projects.features.staking.api) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e219fbf94c..3fa0898e2b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory import com.tangem.domain.tokens.actions.UnreachableActionsFactory import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -54,7 +55,11 @@ class GetCryptoCurrencyActionsUseCase( ) @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { + operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldSupplyAvailability: YieldSupplyAvailability = YieldSupplyAvailability.Unavailable, + ): Flow { return when { cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> { flowOf(value = MissedDerivationsActionsFactory.create()) @@ -74,11 +79,12 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency, ) - .mapLatest { + .mapLatest { stakingAvailability -> outdatedDataActionsFactory.create( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, - stakingAvailability = it, + stakingAvailability = stakingAvailability, + yieldSupplyAvailability = yieldSupplyAvailability, ) } } @@ -94,6 +100,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, stakingAvailability = stakingAvailability, + yieldSupplyAvailability = yieldSupplyAvailability, shouldShowSwapStories = swapStoryContent != null, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index d18e2e36f4..c3850162db 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.withTimeoutOrNull @@ -193,4 +194,13 @@ internal open class BaseActionsFactory( is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired } } + + protected fun ActionAvailabilityBuilder.addYieldSupplyAction(yieldSupplyAvailability: YieldSupplyAvailability) { + if (yieldSupplyAvailability is YieldSupplyAvailability.Available) { + ActionState.YieldMode( + unavailabilityReason = ScenarioUnavailabilityReason.None, + apy = yieldSupplyAvailability.apy, + ).addByReason() + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index bef54bd155..3fb574d9e2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -32,12 +33,14 @@ internal class CommonActionsFactory( * @param userWallet the user's cold wallet * @param cryptoCurrencyStatus the status of the cryptocurrency * @param stakingAvailability the staking availability for the cryptocurrency + * @param yieldSupplyAvailability the yield supply availability for the cryptocurrency * @param shouldShowSwapStories a flag indicating whether to show swap stories */ suspend fun create( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, + yieldSupplyAvailability: YieldSupplyAvailability, shouldShowSwapStories: Boolean, ): Set = coroutineScope { val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) @@ -128,6 +131,10 @@ internal class CommonActionsFactory( // region HideToken addHideTokenAction() // endregion + + // region YieldMode + addYieldSupplyAction(yieldSupplyAvailability) + // endregion } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index f6ff91d2fd..308a2812bd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -31,11 +32,13 @@ internal class OutdatedDataActionsFactory( * @param userWallet the user's cold wallet * @param cryptoCurrencyStatus the status of the cryptocurrency * @param stakingAvailability the staking availability for the cryptocurrency + * @param yieldSupplyAvailability the yield supply availability for the cryptocurrency */ suspend fun create( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, + yieldSupplyAvailability: YieldSupplyAvailability, ): Set = coroutineScope { val sources = cryptoCurrencyStatus.value.sources @@ -129,6 +132,10 @@ internal class OutdatedDataActionsFactory( // region HideToken addHideTokenAction() // endregion + + // region Yield Mode + addYieldSupplyAction(yieldSupplyAvailability) + // endregion } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 1260157432..1cea4893f6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -34,6 +34,11 @@ data class TokenActionsState( data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class YieldMode( + override val unavailabilityReason: ScenarioUnavailabilityReason, + val apy: String, + ) : ActionState() + data class Analytics(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt new file mode 100644 index 0000000000..aa74eb5448 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.yield.supply.models + +/** + * Represents the availability of yield supply for a given asset. + */ +sealed class YieldSupplyAvailability { + data class Available(val apy: String) : YieldSupplyAvailability() + + data object Unavailable : YieldSupplyAvailability() +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt new file mode 100644 index 0000000000..db9db0ea7e --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.yield.supply.YieldSupplyRepository +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability + +class YieldSupplyGetAvailabilityUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(currency: CryptoCurrency): Either = Either.catch { + val tokenCurrency = currency as? CryptoCurrency.Token ?: return@catch YieldSupplyAvailability.Unavailable + + val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty() + val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == tokenCurrency.yieldSupplyKey() } + val yieldSupplyToken = cachedStatus ?: yieldSupplyRepository.getTokenStatus(tokenCurrency) + + if (yieldSupplyToken.isActive) { + YieldSupplyAvailability.Available(yieldSupplyToken.apy.toString()) + } else { + YieldSupplyAvailability.Unavailable + } + } +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index ac6adcc8fb..24d38f3109 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.transaction) + implementation(projects.domain.yieldSupply.models) + implementation(projects.domain.yieldSupply) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index 772d83f651..a547351b4c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -47,6 +47,7 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Receive -> "Button - Receive" TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" TokenActionsBSContentUM.Action.Stake -> "Button - Stake" + TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode" else -> "error" }, params = buildMap { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index ff72822f09..d5522e7218 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.impl.loader +import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce @@ -11,6 +12,8 @@ import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -30,6 +33,7 @@ internal class PortfolioDataLoader @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) { @@ -72,7 +76,10 @@ internal class PortfolioDataLoader @Inject constructor( .flatMapLatest { walletsWithStatuses -> val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> statuses.map { status -> - getCryptoCurrencyActionsUseCase(wallet, status) + val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { + YieldSupplyAvailability.Unavailable + } + getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) .map { PortfolioData.CryptoCurrencyData( userWallet = wallet, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index ce68f5550d..b64ee6bf1f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -71,8 +71,8 @@ internal class PortfolioTokenUMConverter( ): PortfolioTokenUM.QuickActions { return PortfolioTokenUM.QuickActions( actions = toQuickActions(cryptoData.actions), - onQuickActionClick = { - when (it) { + onQuickActionClick = { quickActionUM -> + when (quickActionUM) { QuickActionUM.Buy -> tokenActionsHandler.handle( action = TokenActionsBSContentUM.Action.Buy, cryptoCurrencyData = cryptoData, @@ -89,6 +89,10 @@ internal class PortfolioTokenUMConverter( action = TokenActionsBSContentUM.Action.Stake, cryptoCurrencyData = cryptoData, ) + is QuickActionUM.YieldMode -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.YieldMode, + cryptoCurrencyData = cryptoData, + ) } }, onQuickActionLongClick = { @@ -110,6 +114,7 @@ internal class PortfolioTokenUMConverter( is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge) is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake + is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy) else -> null }?.let(::add) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index c8f42a8843..7561518e9f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -15,11 +15,11 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -65,6 +65,7 @@ internal class TokenActionsHandler @AssistedInject constructor( TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData) } } @@ -178,6 +179,32 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } + private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() + .firstOrNull()?.apy ?: return + + val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> + currencyData.userWallet.walletId to currencyData.status + } + if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) { + router.push( + AppRoute.YieldSupplyActive( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } else { + router.push( + AppRoute.YieldSupplyPromo( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } + } + @AssistedFactory interface Factory { fun create( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt index ffb485d08c..e0987c16e7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.markets.impl.R @Immutable @@ -39,4 +40,12 @@ internal sealed class QuickActionUM( description = resourceReference(R.string.stake_token_description), icon = R.drawable.ic_staking_24, ) + + data class YieldMode( + private val apy: String, + ) : QuickActionUM( + title = resourceReference(R.string.yield_module_start_earning), + description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), + icon = R.drawable.ic_analytics_up_mini_24, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 83450b1ebc..078c25562f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -47,6 +47,10 @@ internal data class TokenActionsBSContentUM( text = resourceReference(R.string.common_stake), iconRes = R.drawable.ic_staking_24, ), + YieldMode( + text = resourceReference(R.string.yield_module_start_earning), + iconRes = R.drawable.ic_analytics_up_mini_24, + ), ; val order: Int = ordinal diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index c6ef0d9981..ad35b058ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -42,6 +42,7 @@ internal class MultiWalletCurrencyActionsConverter( } } + @Suppress("LongMethod") private fun mapTokenActionState( actionsState: TokenActionsState.ActionState, cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -105,6 +106,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_analytics_24 action = { clickIntents.onAnalyticsClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.YieldMode -> { + title = resourceReference(R.string.yield_module_start_earning) + icon = R.drawable.ic_analytics_up_mini_24 + action = { /* no-op */ } + } } return TokenActionButtonConfig( From 7e02e72a40a654c2dd40b4106d907559c09bcc7c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 16:26:46 +0500 Subject: [PATCH 073/130] Updated on 2026-08-14 --- .../core/ui/components/fields/PinTextField.kt | 2 +- .../accesscode/AccessCodeComponent.kt | 12 +++++++++++ .../hotwallet/accesscode/AccessCodeModel.kt | 20 +++++++++++++------ .../accesscode/entity/AccessCodeUM.kt | 3 +++ .../hotwallet/accesscode/ui/AccessCode.kt | 9 ++++++++- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index c348c0933f..9ee27e68ec 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -39,8 +39,8 @@ fun PinTextField( pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, ) { - val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { TextFieldValue(value, selection = TextRange(value.length)) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index ec2f7d3ca4..08693225cc 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.accesscode import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect import com.tangem.features.hotwallet.accesscode.ui.AccessCode import com.tangem.domain.models.wallet.UserWalletId @@ -31,10 +35,18 @@ internal class AccessCodeComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + EventEffect(event = state.requestFocus) { + focusManager.clearFocus() + focusRequester.requestFocus() + } DisableScreenshotsDisposableEffect() AccessCode( modifier = modifier, + focusRequester = focusRequester, state = state, ) } 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 0e1efd5301..f08755fcab 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 @@ -7,6 +7,8 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction @@ -146,7 +148,11 @@ internal class AccessCodeModel @Inject constructor( title = resourceReference(R.string.access_code_alert_validation_cancel), onClick = { uiState.update { currentState -> - currentState.copy(onAccessCodeChange = ::onAccessCodeChange) + currentState.copy( + accessCode = "", + onAccessCodeChange = ::onAccessCodeChange, + requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent), + ) } }, ), @@ -154,15 +160,17 @@ internal class AccessCodeModel @Inject constructor( title = resourceReference(R.string.access_code_alert_validation_ok), onClick = ::setNewCode, ), - onDismissRequest = { - uiState.update { currentState -> - currentState.copy(onAccessCodeChange = ::onAccessCodeChange) - } - }, + isDismissable = false, ), ) } + private fun consumeRequestFocusEvent() { + uiState.update { currentState -> + currentState.copy(requestFocus = consumedEvent()) + } + } + private suspend fun showErrorAndReset() { uiState.update { currentState -> currentState.copy( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 1bc94c1d7c..978071789b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -1,6 +1,8 @@ package com.tangem.features.hotwallet.accesscode.entity import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( @@ -8,6 +10,7 @@ internal data class AccessCodeUM( val accessCodeColor: PinTextColor, val onAccessCodeChange: (String) -> Unit, val isConfirmMode: Boolean, + val requestFocus: StateEvent = consumedEvent(), ) { val accessCodeLength: Int = ACCESS_CODE_LENGTH } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 5d0526fe6b..ecfa44d9d6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -5,8 +5,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -20,7 +22,11 @@ import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM @Suppress("LongParameterList", "LongMethod") @Composable -internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { +internal fun AccessCode( + state: AccessCodeUM, + modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, +) { Column( modifier = modifier .fillMaxSize() @@ -77,6 +83,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, + focusRequester = focusRequester, ) } } From c2e67ba6bc393870ff0974e81a161afc538ce822 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 16:10:57 +0400 Subject: [PATCH 074/130] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 15 ++ .../tangem/tap/routing/utils/ChildFactory.kt | 5 + .../com/tangem/common/routing/AppRoute.kt | 11 +- .../tangem/datasource/api/pay/TangemPayApi.kt | 3 + .../response/CustomerEligibilityResponse.kt | 15 ++ .../local/preferences/PreferencesKeys.kt | 3 + .../datasource/local/visa/TangemPayStorage.kt | 4 + .../main/res/drawable/ic_tangem_pay_24.xml | 9 ++ .../img_tangem_pay_onboarding_banner.webp | Bin 0 -> 7824 bytes .../pay/DefaultTangemPayEligibilityManager.kt | 94 ++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 8 + .../repository/DefaultOnboardingRepository.kt | 16 +- .../repository/TangemPayRequestPerformer.kt | 13 ++ .../data/pay/util/TangemPayWalletsManager.kt | 38 ----- .../domain/pay/TangemPayEligibilityManager.kt | 8 + .../tangem/domain/pay/model/CustomerInfo.kt | 1 + .../pay/repository/OnboardingRepository.kt | 6 + .../TangemPayMainScreenCustomerInfoUseCase.kt | 15 +- features/details/impl/build.gradle.kts | 2 + .../features/details/model/DetailsModel.kt | 18 ++- .../features/details/utils/ItemsBuilder.kt | 22 +++ .../TangemPayOnboardingComponent.kt | 11 +- .../onboarding/impl/build.gradle.kts | 4 + .../DefaultTangemPayOnboardingComponent.kt | 28 ++++ .../TangemPayWalletSelectorComponent.kt | 39 +++++ .../DefaultOnboardVisaDeepLinkHandler.kt | 4 - .../di/TangemPayOnboardingModelsModule.kt | 8 +- .../model/TangemPayOnboardingModel.kt | 140 +++++++++++------- .../model/TangemPayWalletSelectorModel.kt | 73 +++++++++ .../ui/TangemPayOnboardingNavigation.kt | 13 ++ .../tangempay/ui/WalletSelectorBSContentUM.kt | 10 ++ .../tangempay/ui/WalletSelectorBottomSheet.kt | 124 ++++++++++++++++ .../model/intents/TangemPayClickIntents.kt | 17 +++ .../router/DefaultWalletRouter.kt | 10 +- .../presentation/router/InnerWalletRouter.kt | 3 +- .../wallet/state/model/TangemPayState.kt | 5 + ...TangemPayHideOnboardingStateTransformer.kt | 18 +++ ...ngemPayOnboardingBannerStateTransformer.kt | 25 ++++ .../subscribers/TangemPayMainSubscriber.kt | 14 +- .../visa/TangemPayMainScreenBlock.kt | 1 + .../visa/TangemPayOnboardingBanner.kt | 129 ++++++++++++++++ 41 files changed, 863 insertions(+), 119 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt create mode 100644 core/ui/src/main/res/drawable/ic_tangem_pay_24.xml create mode 100644 core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 4f7f4a0166..80e06699c1 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -115,6 +115,7 @@ internal class DefaultTangemPayStorage @Inject constructor( appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) } override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { @@ -144,6 +145,20 @@ internal class DefaultTangemPayStorage @Inject constructor( } } + override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { + withContext(dispatcherProvider.io) { + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) + } + } + + override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { + return withContext(dispatcherProvider.io) { + appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), + ) == true + } + } + private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId" 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 df8495fe6c..715bf51463 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 @@ -40,6 +40,8 @@ import com.tangem.features.tangempay.components.TangemPayDetailsContainerCompone import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -667,6 +669,9 @@ internal class ChildFactory @Inject constructor( ) is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink( deeplink = mode.deeplink, + ) + is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings + is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain( userWalletId = mode.userWalletId, ) }, 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 cbe8919317..c2d8b480d7 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 @@ -433,13 +433,20 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Deeplink( val deeplink: String, - val userWalletId: UserWalletId?, ) : Mode() @Serializable data class ContinueOnboarding( - val userWalletId: UserWalletId?, + val userWalletId: UserWalletId, ) : Mode() + + @Serializable + data class FromBannerOnMain( + val userWalletId: UserWalletId, + ) : Mode() + + @Serializable + data object FromBannerInSettings : Mode() } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 9c4fca8149..57d31d6a0d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -128,6 +128,9 @@ interface TangemPayApi { @POST("v1/deeplink/validate") suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse + @GET("v1/customer/eligibility") + suspend fun checkCustomerEligibility(): ApiResponse + @GET("v1/order/{order_id}") suspend fun getOrder( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt new file mode 100644 index 0000000000..8ddcfab3f7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CustomerEligibilityResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 037f71a2a6..775e916143 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -192,6 +192,9 @@ object PreferencesKeys { fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) = booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId") + fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) = + booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId") + // endregion } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index e32fe05975..a9279e9193 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -30,5 +30,9 @@ interface TangemPayStorage { suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean + + suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) + suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_tangem_pay_24.xml b/core/ui/src/main/res/drawable/ic_tangem_pay_24.xml new file mode 100644 index 0000000000..42130a709f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_pay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp b/core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp new file mode 100644 index 0000000000000000000000000000000000000000..e3a0bccac9ba155267f97b25906b2cb8fa33cd51 GIT binary patch literal 7824 zcmV;B9&h1NNk&G99smGWMM6+kP&il$0000G0000i0RX1~06|PpNN@uH00EGr?Vp=C zGyf4nl+K|dBUC$#_6hn;y}iHzHXML(0&@bm0lvXJk0f&XJ!63)0)iV! ziloTf*vlF?Sf&p!e|}O&#YS7YX|)=)+C#B;adEM``+a@wb0z}@uP<71kaRogYNgU` zKEJoOx4yn%n(t=xcjs+6vb0;RdcE$RbcK_>oV{UN*83U9VOx$}{&`Nir&764ILYN~ z+x}`!*>LSg0&$@;QB$ZmmE z<_#Y?__J2=XKBt5OJ&IiznQNJPEH(5j37Zm;3Q4ckV0qzDuIQ26vC-EAtS_1l;k90 z=qSlUB!ZGUfJvHHNUZ`@p$JZRi-A)iphOx}WB1s>KnWs&R1r>)DxR?c6auNSATkW7 zgcC#rl{`d@3{XTIU@F2XFyXEO6~qlA0@VFGq(lO%F}Ve)9q}+u^4u8?W2LZ>2I`SR z7$y;SfNIQvD&!p_C1)IlNt{TB3hoG`!ntP^Jq;4%f>Ys6pc*5!8Fv%bu_{Q3xUS;_ zd6YtFN6bOjK@wx+uA-wP#u)=joMTlu<9Z0D;wGerQZN~cA9VDSbPpT_vDXc(B8dzuw3#>L1U`c2RWi{5+mQ)o2 zEeWU)SQR0lLTO3FRTYF+`9C-&!fF*#S0Z&a7H~;n!PS*GRN_Qn5rYa26&x!VSVD^^ zR4}l{QB^f&OkJ(QI8#!kBVZ8?{3r0Az<&b&3H&GUpTK_t|C#560aj2rAo3aj0I-|@ zodGH>0jL2!kwlzJC8MJvqcZDAuo4MpZGQ0E|J%N1+P|MG#t2&z{XTkEILR|yOottB zdRO#)2}RedKM_wy^$CC8-~G8C{@>!>eKxInTmFgY+t2TJ_ts~k*Z+66H~zo%-q64A zwEMoT=F96paJKU-$;;Qbyzq9UcW;wdtMeUHv5@9&{2= zgDb^n_!(e+65^<%dqxNNk51Y2(q8_DzuH(GML>W848T6$V`^bTO^wAiHye~AMUPL1v_r6H0l7rK*I2! zMsEmZ{y9;ER)u7xbdEr>pk7gj)xbP8_~Rk`nAT2R6`h+u?y*{g4{ z3ba4G$i@r*b~YGvc1P|h-;WgTk+*33VLBmAHU_30byF4mbl{e?e67X87=^_&fJszv z?aC4wmmX&4+|DSy8cCDiyBpVYQBnuA`v=s9Tc(E&_J@$#FVQBWymT~tbMo2w`qlqt z`58+KT<0A#LuH_CdMjtH9IluJJ_oqh<1k@srIZcEupJu=5PmN5G&Gq+XWdJL5n=>} zk6s`TdZFZd|#%bolb)uYK)XbJJtk11PCWXBQ!BDjpnMjP$&$Nc39#E_w3 zg+u&~x*I@cX`#l-Z`(-FDMOjC)W5I@LXS)##Aoc1dA`@0V0h(E8zG5bO#;-N6!d5F z^7zi1vDIl3c{zW;$JcWEu4WO8CPEla}i`7yDxW47pU^34Uqsp@o_gMIykugG^>}68m}3*A^J)k({xEc2m(* zn7uRMkA9^Wx)fdzA4Y#AC%|A9rUG7841LA0tO=R z)b$Ni;KS!f0owK^XvuwF%H=`cAB=o zF#+_P2otv-{<@Q{30zj+@B=Sj>&J^K?I{nyC4`xqo;-fmX^O(QP-{2eB7uhb9L340 zXz%>KU5S649J62|Q~AcF7(?tbI#$(2 zSG63{omeW;$}7}U5-uyM7kqE01651=TgM=HtY&T!7w4?I!Oq=M9kVz&D~_97jH=PS z{Jx>;OMdRPLKMh6YeiKXeC+(ZM>K9lvF#(c4~-kG^1FHZ$ zN@x#gC|oLm=;l`2&E%pl;ZTtV(CIZa?fu({%j8A(mSMP0nBfgM4=kIF$8p~_|K>41 zWkavdg}`@AIC!&kA7xZfB&fY_1Zk}HN3%rB%t!n}HY}HJ6$TsEM{Rk`pjKyhF>;z- zc@>-MgtjmnfG5aQlF5Ssgg4TA4EFFvBC_(?33P#e3)2z?n3&P1LYvn_dmD~++Xesl z=jpex8JP-BoAN_($T}02EC=8_vMvic=LH&JA0#kCA7ST0 z;}WY0RxomCR5#_6=c9GF^c9r-9em79ko<)HItV7+g)!ht!i6|=_uP02Y&_!U`$aTo zB=-Y3)jf=FhpeyfB+l1H6g5?pznJse6XHp~2>GPY1ukEqIVZB%Js81QQ^oi8uN(%d zWjR%1)h=4dLNnrdkwcOX<8m_fhY3C@rUSU`#G~Y7U4jCUbeq#RP=K_C&`YdLE+-$$ zOTR;7(9No;m8C1Xl$JF~Joq^Rf}?m-XhblQsE8JufiO=9hUFgnCga*i4Tf9507y5f ztdRM&sc%iLpCM(DW@0^OPpwQV4A1d%zvM?pHC2)ROW03|U^4LDVW@8A)AJgdYs3E) z;Fo8GQdmfaW*za9*YtxAauUQ!>$8w;-h=)!wqOfO&t7O-L2dE}j4T*YA85Zr1n*`Q zJhmG8;<}_rjGryqz`~5nVerb)?1FblX3bPczFw=n*hgW24I!Hlfv+#M3^MWz&q_#=2`1X8$S@>~MY=Aj=GS`bnomdkWC{C^?$+rN zlM)j<$Vd#T9sU42Z^Z0ZMKbc=P+(_C0P&2Zlbmh!O9wVp!v#p?sVZ;qu366bG`PXa z5$54G?R&iU3r*gGG8CoKYCX*k#YxrI2ioP6Y}SmvfMlih-D*qH6_Az`z}<86M(E|DGQL1Rbc6XvqMu%f7jc2j7faDq3{K0%A8! z8*AP&Rv{(CK85iB#n*oYO&tpy0lK5lWcZkr?o12(f!DfXJ5?@t!p% z;FwDTR0ZBs)o3(`0P$*xn+rM?3X=#lkp@Sf-8Pr<-xma{QNq=l64v&@)7dTGRZvTAgp-!%;CgUK12FMoLNVwagu#phe`1-zQwFPh65>9D^ZlE zblg3Not$ule42%_bMq?fM`sp{81DV<>CjuPACb@=J^LF)r&!>vKSlfzEY$`#@J%* z{Snb4%%q)lnoM$RWAM>!|CUuF$$2=N&|vQyt%&tUvIgnCn%s{t-Uu=CJATl1deQ9KT=v(_i= z@N16tpz(rD5peDCQ`{5Sb_Uo6D*`d+w22CBa4ox2!%i=T{gq}2Y+o0fyVZNC1L%m4?`wkF9fW}tY9=v8a`4nnf74Ck?e`q6$W!ZtbJ;~-m;BM9ak2Z zQ`Fa;uU<4`R90&jKuVB#r-un7ZDeW~j2!#eH_0J96kct^`uJD2qfIhDAEids!eRYB z`s1JHZH>MdNb$<|=7nJ%;bGFRsDh57X!5Np;{}0-Rf)E{(Ha z+`M6(fmIq}#=nz8T-D<{6wMVbsNpeu9zjo}*!MA_Bc4Rm!Xn)?Ua^&oq}zZsgQeD} z5{6j2G&OjAz^L~_$QE7dXxH1syd(H6)uLRMb9vB7A`gdPR3C3K84A(EjAR`rT}$eC;@z5%aq?#nH?Ko zRITap{~f|4^TA(h<`@gimPQ&re%`Hn>;o!+&vY=8mMC$Cqp~z#WD|~QVMI_+iz@jF zKp3u1h0+x8)d#L;r3ytB?hE?lz6cQ2bms?NRA1Mlonp#E7h!56ffP z%t<1SG>~dy+~?FpLSOgAeP%i!`CzQI&)&#`{NH6Gg2)+F9A0iH6#BOM?MRxYVBZJe zWyKRviSel)J1;)Cevob9YprSrl+J8x^>*PXpJz#m*L*|B0$j7{pI(T}@@-j8Q_vOv zO2u0tPqK~xd5)yv5TA`3DD)@4Rc;SFzAG1$eH0W$ZOg(xj>+|r_C6+gUb8zq5b?6? z&!c?5pcyPex{JmBQcZ*o#5^k?w%wxBY~0n70!0{4Vvrjv-m-5gThxg5gzt760cUdj z;*~AD=ga@7HoQ{oXw2rJ&73Vxci4Gz9aK+`Pz40&i^p7t7V}ciu<6LX^C*0r-1|j` z1M3YN#Z4x2iJe;-tVSJ)<;`AI`zEyBtQ+%$v!jHb&hsBv&-gD(XG6Nhtl}Uh^tvwj z%hskrNMn!4@Z_zT*E6qSWdcFJ?L?MAa7^Y1x%jI56%*3Iq)VScdr?>jCTWeP-I zX?d(bRz8{y)$QgI)cyXA5W?fRc`U=N5J~{FP>aMe$x1yyt_%Q9J<3P&=93zH^r8pa zCQ|d&h@+te*ddJt>-am9w;VJ1VHYQw{jUHIWVGcHTapX7faagYHpOCN`S(_C|IkS6ibF(O5yK-7W4lEvr&;1PJ6O^ZG!=YY4#^xx}HF>HreJ#!_8moCp_gc zPIOK90Upij?ydDsq&Z$s6hX#~*a>9y>U9RvlScr`#_)3ke%P`GwiH`fMk&kX{7(3M zcn`LkKQpp1fa!fsk)z-|*`cF&I+*{KsJwRFW?24HgddT&LIsjmI37e7!oR;y@-12WH9r>kxC{B^ z&3~eZkx4%v*%hW8Y(0hAGR7$2s}_s%ZXr@5Qo2bB-vv{>Lvjx!q<_xBdkGc(RlnkH zi2wH4e8!(DoFl8WKud*1>6>r*-g-DauK; zkjQJ^%9nP}k^_>EF=XXPf!60DR?cS`xKA2uR?YxY^RtoO>K;P}nQZ#v$~f-<08y zr_r~*d+5+RSnaKLc@UrIONdomD_xC|6uOPixf4mf6LY}%pWD2xrZAPs7_>B+yl;3U zTjLNPT0PNyn*n$!IG`v}>=Bm-qO%!tBMOmt=_ z%{c||0E&HWzP^FkYPQ1-$oz;bo}5o zvn*Hn1PHf-a(c0T1lg_95}h3IP5=tXBJ^)O@qne^TVBEnwxr6%LdI?}XB{Ht^}2aN zlS4DRk}!_R$n=-G!EZ1WJtAWY#An>Xl7h|wPHtSuqgW%QS8g&68PFHcC)Ni~EXF-# zFUSz$N0yF#(KB7g^bbohRMB^nO(P%W!zY@~g81Gee-BAA+tyed7Qv4XE?nS1RMiCx z)!O;WTv0QXu`QUCBwErNhipg`#YPvlBT+TivHh!Cl1dkaMuX`X2YkaACW?v3-FhDj zDqxMplE|YpJsCov^&nC<@i@ztjNYRg$&*dFYo zC^4;Oy8m~MF5hME{{9$Rn4oKV#u5#T-}7BGKFh%Q^Gi5_Qr;m*l9Dgx!1jNN_`3jK z0x|Mg(KUPl%j};u?F{)dZw9WyBPx7F_J|PvUk%vhp;lZySU6$J*qD87N^4?Zp*dPDX8LhB1HqHe$U{Ge2^bo<_Gt!kTg{}Kf4RxL_suDMl__z|E|TL&N;%TNwvwZb8;f8~ZG062Sf|<;cp1p-0bmeyJr#eI z=ewzJsDv^H#@_q_+lu7v)yy#oEp=Gr)$91@o&DKq0&o7CWyAh*1EGID!W4WukU5-} z!`VJ%%7}z-e>f?m9-v^QLGY97Is5504Q&k@*JdU-&Nz+r{Ty_*=|>H8h>^e2pr3?j zPw1zAK7EoZm`oJ1I)tQ{+r=%6d@J592fEVpWle6xkc)op*%|p5{N4@zIbEfXYAQl| zUi9k&vLjr4phui^ozX(n$t*Q$eTfDaL%nDMYN(vv(VJ2UfX+a`uF#&>d z00J|ir`vVQfDhqTou$4^Gg-1q*iBP-Jk$eIs8!X+eOmFgA(s#eKOpQa?2+;vK4HK8CT-Iq@rl2({heO>9-DELh zKa+0+e;+DVP(45+H6Q$_5MtT4!@CWwZTD4RSTcx!5q(2-(-tD*D;{wMLt?11uw@8Q zahL4S2MybJlNmLa7r7P&BFzVFkb%#bjq9NeoVovMW`5bdjfPM<3g4ihh2o^exM#Dr zNAW^|r8%G8MLoQNdz^suduD3VRsx^p2^3M@E5lockqjSuI zcHZM)?G)Jcz1gjr5Y!2%myQi*$P&n8S7T#REkZ}W%M4cwHH5xOGPcO&Tya;^uS1Wy zy5d~CvyDK3IBV!GE)(ez?G_7MkoaQ)Rz?=!0V`QqHnGjFcEjSKfLaz`i@p7)E9xrJ371uX2Iohb8q|XCEsAyyH3S2*No~57X+d@VtoPhuqw) z0L^C{#|k9yI0SV? = null + private var eligibleWalletsDeferred: Deferred>? = null + private val loadMutex = Mutex() + + override suspend fun getEligibleWallets(): List { + cachedEligibleWallets?.let { return it } + + return loadMutex.withLock { + cachedEligibleWallets?.let { return it } + eligibleWalletsDeferred?.let { return it.await() } + + coroutineScope { + val deferred = async { + getPossibleWalletsForTangemPay() + .excludePaeraCustomers() + .also { cachedEligibleWallets = it } + } + eligibleWalletsDeferred = deferred + try { + deferred.await() + } finally { + eligibleWalletsDeferred = null + } + } + } + } + + private suspend fun getPossibleWalletsForTangemPay(): List { + if (!onboardingRepository.checkCustomerEligibility()) { + return emptyList() + } + + val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.value + } else { + userWalletsListManager.userWalletsSync + } ?: return emptyList() + + return wallets.filter { wallet -> + wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() + } + } + + private fun UserWallet.isCompatible(): Boolean = when (this) { + is UserWallet.Cold -> + scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> true + } + + private suspend fun List.excludePaeraCustomers(): List { + if (isEmpty()) return this + + return coroutineScope { + map { wallet -> + async { + val isCustomer = onboardingRepository + .checkCustomerWallet(wallet.walletId) + .getOrNull() == true + wallet to isCustomer + } + } + .awaitAll() + .mapNotNull { (wallet, isCustomer) -> + wallet.takeUnless { isCustomer } + } + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index bae2d97baf..ff79c711bf 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,10 +1,12 @@ package com.tangem.data.pay.di import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory +import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.repository.* import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -62,6 +64,10 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase + @Binds + @Singleton + fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager + companion object { @Provides @Singleton @@ -69,11 +75,13 @@ internal interface TangemPayDataModule { repository: OnboardingRepository, customerOrderRepository: CustomerOrderRepository, tangemPayOnboardingRepository: OnboardingRepository, + eligibilityManager: TangemPayEligibilityManager, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( repository = repository, customerOrderRepository = customerOrderRepository, tangemPayOnboardingRepository = tangemPayOnboardingRepository, + eligibilityManager = eligibilityManager, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 563b3ee0eb..2e66473cae 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -84,7 +84,6 @@ internal class DefaultOnboardingRepository @Inject constructor( } override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either { - // TODO implement selector return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) } .map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) } } @@ -193,4 +192,19 @@ internal class DefaultOnboardingRepository @Inject constructor( error } } + + override suspend fun checkCustomerEligibility(): Boolean { + val response = requestHelper.performWithoutToken { + tangemPayApi.checkCustomerEligibility() + }.getOrNull() + return response?.result?.isTangemPayAvailable == true + } + + override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { + return tangemPayStorage.getHideMainOnboardingBanner(userWalletId) + } + + override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) { + tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true) + } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 788aafe9d8..83e30d675e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -89,6 +89,19 @@ internal class TangemPayRequestPerformer @Inject constructor( ) } + suspend fun performWithoutToken(requestBlock: suspend () -> ApiResponse): Either = + withContext(dispatchers.io) { + catch( + block = { + when (val apiResponse = requestBlock()) { + is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left() + is ApiResponse.Success -> apiResponse.data.right() + } + }, + catch = { errorConverter.convert(it).left() }, + ) + } + suspend fun performRequest( userWalletId: UserWalletId, requestBlock: suspend (header: String) -> ApiResponse, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt deleted file mode 100644 index 4423dddf32..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.data.pay.util - -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import javax.inject.Inject - -// TODO remove after implement wallet selector in pay -class TangemPayWalletsManager @Inject constructor( - private val manager: UserWalletsListManager, - private val repository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) { - - @Deprecated("Don't use and put userWallet in features that need it") - suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold { - val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets - val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first() - return findColdWallet(userWallets) - } - - @Deprecated("Don't use and put userWallet in features that need it") - fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold { - val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync - return findColdWallet(userWallets) - } - - private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled - - private fun findColdWallet(userWallets: List?): UserWallet.Cold { - return userWallets?.find { - it is UserWallet.Cold && it.isMultiCurrency - } as? UserWallet.Cold ?: error("Cannot find cold user wallet") - } -} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt new file mode 100644 index 0000000000..b68cd25513 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.wallet.UserWallet + +interface TangemPayEligibilityManager { + + suspend fun getEligibleWallets(): List +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index ad9583243f..d747e41066 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -5,6 +5,7 @@ import java.math.BigDecimal sealed class MainCustomerInfoContentState { object Loading : MainCustomerInfoContentState() + object OnboardingBanner : MainCustomerInfoContentState() data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState() } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 58046210b6..e3fc78e653 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -25,5 +25,11 @@ interface OnboardingRepository { suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + suspend fun checkCustomerEligibility(): Boolean + fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? + + suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean + + suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 07950a5cec..b84b7f061b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository @@ -21,6 +22,7 @@ class TangemPayMainScreenCustomerInfoUseCase( private val repository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, private val tangemPayOnboardingRepository: OnboardingRepository, + private val eligibilityManager: TangemPayEligibilityManager, ) { val state: StateFlow>> @@ -46,8 +48,17 @@ class TangemPayMainScreenCustomerInfoUseCase( .map(MainCustomerInfoContentState::Content) updateState(userWalletId, result) } else { - // ignore if there's no TangemPay - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + // if there's no tangem pay, check eligibility and show onboarding banner + val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId } + if (isEligible) { + if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } else { + updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) + } + } else { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } } }, ) diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 8f41e0f6c1..8da459c047 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.features.tester.api) implementation(projects.features.createWalletSelection.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.tangempay.details.api) /* Project - Core */ implementation(projects.core.decompose) @@ -46,6 +47,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.legacy) + implementation(projects.domain.visa) /* SDK */ // TODO: For TangemError model, should be removed after card domain scanning refactoring 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 be5d74ca8e..69fe323b69 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 @@ -20,6 +20,7 @@ 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.pay.TangemPayEligibilityManager import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase @@ -34,6 +35,7 @@ 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.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import kotlinx.collections.immutable.ImmutableList @@ -51,7 +53,7 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class DetailsModel @Inject constructor( socialsBuilder: SocialsBuilder, - itemsBuilder: ItemsBuilder, + private val itemsBuilder: ItemsBuilder, private val appVersionProvider: AppVersionProvider, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, @@ -68,6 +70,8 @@ internal class DetailsModel @Inject constructor( private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, + private val tangemPayEligibilityManager: TangemPayEligibilityManager, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, ) : Model() { private val params: DetailsComponent.Params = paramsContainer.require() @@ -100,6 +104,8 @@ internal class DetailsModel @Inject constructor( ), ) + addTangemPayItemIfEligible() + state = MutableStateFlow( value = DetailsUM( items = items.value, @@ -239,6 +245,16 @@ internal class DetailsModel @Inject constructor( } } + private fun addTangemPayItemIfEligible() { + if (!tangemPayFeatureToggles.isTangemPayEnabled) return + modelScope.launch { + val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty() + if (isEligible) { + items.update { itemsBuilder.addVisaItem(it) } + } + } + } + private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" private suspend fun buildBuyLink(): String { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 617de52a47..0cdd6b6a10 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -37,6 +37,17 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ).let(::add) }.toImmutableList() + fun addVisaItem(items: ImmutableList): ImmutableList { + return items.toMutableList().map { block -> + if (block.id == "shop" && block is DetailsItemUM.Basic) { + val newItems = block.items.toMutableList().apply { add(getVisaItem()) } + block.copy(items = newItems.toImmutableList()) + } else { + block + } + }.toImmutableList() + } + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( @@ -114,4 +125,15 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ).let(::add) }.toPersistentList(), ) + + private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item( + id = "get_tangem_visa", + block = BlockUM( + text = resourceReference(R.string.details_get_visa), + iconRes = R.drawable.ic_tangem_pay_24, + onClick = { + router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) + }, + ), + ) } \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index 408d809672..df83da98da 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -8,16 +8,19 @@ interface TangemPayOnboardingComponent : ComposableContentComponent { sealed class Params { - abstract val userWalletId: UserWalletId? - data class Deeplink( val deeplink: String, - override val userWalletId: UserWalletId?, ) : Params() data class ContinueOnboarding( - override val userWalletId: UserWalletId?, + val userWalletId: UserWalletId, ) : Params() + + data class FromBannerOnMain( + val userWalletId: UserWalletId, + ) : Params() + + data object FromBannerInSettings : Params() } interface Factory : ComponentFactory diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 7b71ee3d94..c94b59ab29 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { implementation(projects.features.tangempay.onboarding.api) implementation(projects.features.tangempay.details.api) implementation(projects.features.kyc.api) + implementation(projects.features.wallet.api) + implementation(projects.features.hotWallet.api) /** Domain */ implementation(projects.domain.visa) @@ -40,6 +42,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.decompose.ext.compose) /** DI */ implementation(deps.hilt.android) @@ -48,4 +51,5 @@ dependencies { /** Other */ implementation(deps.timber) implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 4ee5eb136c..e11789c134 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -4,9 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.ComponentContext import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.tangempay.model.TangemPayOnboardingModel +import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -19,10 +25,32 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( private val model: TangemPayOnboardingModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TangemPayOnboardingNavigation.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() TandemPayOnboardingScreen(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + navigation: TangemPayOnboardingNavigation, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + val context = childByContext(componentContext) + return when (navigation) { + is TangemPayOnboardingNavigation.WalletSelector -> TangemPayWalletSelectorComponent( + appComponentContext = context, + params = TangemPayWalletSelectorComponent.Params(listener = model, walletsIds = navigation.walletsIds), + ) + } } @AssistedFactory diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt new file mode 100644 index 0000000000..d8e84fe4e0 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel +import com.tangem.features.tangempay.ui.WalletSelectorBottomSheet + +internal class TangemPayWalletSelectorComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayWalletSelectorModel = getOrCreateModel(params = params) + + override fun dismiss() { + model.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + WalletSelectorBottomSheet(state = state) + } + + data class Params( + val listener: WalletSelectorListener, + val walletsIds: List, + ) +} + +internal interface WalletSelectorListener { + fun onWalletSelected(userWalletId: UserWalletId) + fun onWalletSelectorDismiss() +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt index cb6bf2f491..8c9400fe8e 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -13,15 +12,12 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( @Assisted uri: Uri, appRouter: AppRouter, tangemPayFeatureToggles: TangemPayFeatureToggles, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, ) : OnboardVisaDeepLinkHandler { init { if (tangemPayFeatureToggles.isTangemPayEnabled) { - val userWallet = getSelectedWalletSyncUseCase.invoke().getOrNull() val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( deeplink = uri.toString(), - userWalletId = userWallet?.walletId, ) appRouter.push(AppRoute.TangemPayOnboarding(mode)) } else { diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt index b4b1892e56..3cd13c821b 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.tangempay.model.TangemPayOnboardingModel +import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,5 +17,10 @@ internal interface TangemPayOnboardingModelsModule { @Binds @IntoMap @ClassKey(TangemPayOnboardingModel::class) - fun bindModel(model: TangemPayOnboardingModel): Model + fun bindTangemPayOnboardingModel(model: TangemPayOnboardingModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayWalletSelectorModel::class) + fun bindTangemPayWalletSelectorModel(model: TangemPayWalletSelectorModel): Model } \ No newline at end of file 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 5b9fed7d81..ff881de155 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 @@ -1,6 +1,9 @@ package com.tangem.features.tangempay.model import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -8,17 +11,16 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener -import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import com.tangem.features.tangempay.components.WalletSelectorListener import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer +import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -40,27 +42,33 @@ internal class TangemPayOnboardingModel @Inject constructor( private val repository: OnboardingRepository, private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, private val urlOpener: UrlOpener, - private val tangemPayWalletsManager: TangemPayWalletsManager, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, -) : Model() { + private val eligibilityManager: TangemPayEligibilityManager, +) : Model(), WalletSelectorListener { private val params = paramsContainer.require() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow field = MutableStateFlow(getInitialState()) init { analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened()) + init() + } + + private fun init() { modelScope.launch { when (params) { - is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { - checkCustomerInfo() - } is TangemPayOnboardingComponent.Params.Deeplink -> { repository.validateDeeplink(params.deeplink) .onRight { isValid -> if (isValid) showOnboarding() else back() } .onLeft { back() } } + is TangemPayOnboardingComponent.Params.ContinueOnboarding, + is TangemPayOnboardingComponent.Params.FromBannerInSettings, + is TangemPayOnboardingComponent.Params.FromBannerOnMain, + -> showOnboarding() } } } @@ -78,42 +86,25 @@ internal class TangemPayOnboardingModel @Inject constructor( } } - private suspend fun checkCustomerInfo() { - // TODO implement selector - val userWalletId = getUserWalletForPay(params.userWalletId) - repository.getCustomerInfo( - userWalletId = userWalletId, - ) - // selector - .onRight { customerInfo -> - when { - !customerInfo.isKycApproved -> { - when (params) { - is TangemPayOnboardingComponent.Params.Deeplink -> showOnboarding() - else -> openKyc() + private fun checkCustomerInfo(userWalletId: UserWalletId) { + modelScope.launch { + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) + repository.getCustomerInfo( + userWalletId = userWalletId, + ) + .onRight { customerInfo -> + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) + when { + !customerInfo.isKycApproved -> { + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> openKyc(userWalletId) + else -> startOnboarding(userWalletId) + } } + else -> back() } - else -> back() } - } - .onLeft { back() } - } - - private fun getUserWalletForPay(userWalletId: UserWalletId?): UserWalletId { - val userWallet = userWalletId?.let { getUserWalletUseCase(it).getOrNull() } - return if (userWallet?.isMultiCurrency == true) { - userWallet.walletId - } else { - tryGetSelectedWalletId() - } - } - - private fun tryGetSelectedWalletId(): UserWalletId { - val selectedWallet = getSelectedWalletUseCase.sync().getOrNull() - return if (selectedWallet?.isMultiCurrency == true) { - selectedWallet.walletId - } else { - tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId + .onLeft { back() } } } @@ -122,21 +113,52 @@ internal class TangemPayOnboardingModel @Inject constructor( urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } - @Suppress("NullableToStringCall") private fun onGetCardClick() { analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) + modelScope.launch { + val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId } + if (eligibleWalletsIds.isEmpty()) { + back() + return@launch + } + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { + checkCustomerInfo(params.userWalletId) + } + is TangemPayOnboardingComponent.Params.FromBannerOnMain -> { + if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) { + checkCustomerInfo(userWalletId = params.userWalletId) + } else { + openWalletSelectorIfNeeds(eligibleWalletsIds) + } + } + is TangemPayOnboardingComponent.Params.Deeplink, + is TangemPayOnboardingComponent.Params.FromBannerInSettings, + -> { + openWalletSelectorIfNeeds(eligibleWalletsIds) + } + } + } + } + + private fun openWalletSelectorIfNeeds(eligibleWalletsIds: List) { + if (eligibleWalletsIds.size == 1) { + checkCustomerInfo(userWalletId = eligibleWalletsIds[0]) + } else { + bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(eligibleWalletsIds)) + } + } + + private fun startOnboarding(userWalletId: UserWalletId) { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) modelScope.launch { - // TODO implement selector - val userWalletId = getUserWalletForPay(params.userWalletId) val result = produceInitialDataUseCase(userWalletId) if (result.isLeft()) { - Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") + val errorMessage = result.leftOrNull()?.message ?: "Unknown error" + Timber.e("Error producing initial data: $errorMessage") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } - - // TODO implement selector repository.getCustomerInfo( userWalletId = userWalletId, ).fold( @@ -148,20 +170,17 @@ internal class TangemPayOnboardingModel @Inject constructor( if (customerInfo.isKycApproved) { back() } else { - openKyc() + openKyc(userWalletId) } }, ) } } - private fun openKyc() { - // TODO implement selector + private fun openKyc(userWalletId: UserWalletId) { router.replaceAll( AppRoute.Wallet, - AppRoute.Kyc( - userWalletId = getUserWalletForPay(params.userWalletId), - ), + AppRoute.Kyc(userWalletId = userWalletId), ) } @@ -172,4 +191,13 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun getInitialState(): TangemPayOnboardingScreenState { return TangemPayOnboardingScreenState.Loading(onBack = ::back) } + + override fun onWalletSelected(userWalletId: UserWalletId) { + bottomSheetNavigation.dismiss() + checkCustomerInfo(userWalletId) + } + + override fun onWalletSelectorDismiss() { + bottomSheetNavigation.dismiss() + } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt new file mode 100644 index 0000000000..27a15aeeeb --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt @@ -0,0 +1,73 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.features.tangempay.components.TangemPayWalletSelectorComponent +import com.tangem.features.tangempay.ui.WalletSelectorBSContentUM +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayWalletSelectorModel @Inject constructor( + paramsContainer: ParamsContainer, + userWalletsFetcherFactory: UserWalletsFetcher.Factory, + messageSender: UiMessageSender, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + private val userWalletsFetcher = userWalletsFetcherFactory.create( + messageSender = messageSender, + onlyMultiCurrency = true, + isAuthMode = false, + isClickableIfLocked = false, + onWalletClick = { params.listener.onWalletSelected(it) }, + ) + + init { + fetchUserWalletsUM() + } + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun getInitialState(): WalletSelectorBSContentUM { + return WalletSelectorBSContentUM( + userWallets = persistentListOf(), + onDismiss = { params.listener.onWalletSelectorDismiss() }, + ) + } + + private fun fetchUserWalletsUM() { + modelScope.launch { + val eligibleWalletsIds = params.walletsIds.map { it.stringValue }.toSet() + userWalletsFetcher.userWallets.collectLatest { userWalletsListUM -> + uiState.update { + WalletSelectorBSContentUM( + userWallets = userWalletsListUM + .filter { wallet -> wallet.id in eligibleWalletsIds } + .toImmutableList(), + onDismiss = { params.listener.onWalletSelectorDismiss() }, + ) + } + } + } + } + + fun onDismiss() { + params.listener.onWalletSelectorDismiss() + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt new file mode 100644 index 0000000000..8fe01286d7 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tangempay.ui + +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class TangemPayOnboardingNavigation { + + @Serializable + data class WalletSelector( + val walletsIds: List, + ) : TangemPayOnboardingNavigation() +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt new file mode 100644 index 0000000000..e0028184e6 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.ui + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class WalletSelectorBSContentUM( + val userWallets: ImmutableList, + val onDismiss: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt new file mode 100644 index 0000000000..fd3f9a755e --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt @@ -0,0 +1,124 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.onboarding.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletSelectorBottomSheet(state: WalletSelectorBSContentUM) { + TangemBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = state.onDismiss, + containerColor = TangemTheme.colors.background.tertiary, + title = { content -> + TangemTopAppBar( + title = resourceReference(R.string.common_choose_wallet), + titleAlignment = Alignment.CenterHorizontally, + endButton = TopAppBarButtonUM.Close(onCloseClick = state.onDismiss), + ) + }, + ) { content -> + Content( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, + ), + state = state, + ) + } +} + +@Composable +private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()), + ) { + BlockCard( + modifier = Modifier.fillMaxSize(), + colors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) { + state.userWallets.forEach { state -> + key(state.id) { + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + state = state, + ) + } + } + } + SpacerH(bottomBarHeight) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewWalletSelectorBottomSheet() { + TangemThemePreview { + WalletSelectorBottomSheet( + state = WalletSelectorBSContentUM( + userWallets = persistentListOf( + UserWalletItemUM( + id = "1", + name = stringReference("Wallet 1"), + information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), + balance = UserWalletItemUM.Balance.Loading, + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.None, + onClick = {}, + ), + UserWalletItemUM( + id = "2", + name = stringReference("Wallet 2"), + information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), + balance = UserWalletItemUM.Balance.Loading, + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.None, + onClick = {}, + ), + ), + onDismiss = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index ed0005c791..b00b4b5886 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R @@ -14,6 +15,7 @@ import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,6 +31,10 @@ internal interface TangemPayIntents { fun onIssuingFailedClicked() fun onPaySupportClick() + + fun onOnboardingBannerClick(userWalletId: UserWalletId) + + fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) } @Suppress("LongParameterList") @@ -115,4 +121,15 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( ) } } + + override fun onOnboardingBannerClick(userWalletId: UserWalletId) { + router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) + } + + override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) { + modelScope.launch { + stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId)) + onboardingRepository.setHideMainOnboardingBanner(userWalletId) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 98d68adf5f..934d258f18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -111,14 +111,8 @@ internal class DefaultWalletRouter @Inject constructor( ) } - override fun openTangemPayOnboarding(userWalletId: UserWalletId) { - router.push( - AppRoute.TangemPayOnboarding( - AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding( - userWalletId = userWalletId, - ), - ), - ) + override fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) { + router.push(route = AppRoute.TangemPayOnboarding(mode = mode)) } override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index cf9d3ea617..c423b80481 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.common.routing.AppRoute import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -63,7 +64,7 @@ internal interface InnerWalletRouter { fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) - fun openTangemPayOnboarding(userWalletId: UserWalletId) + fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt index bbe8472d49..25c8c2f0a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -11,6 +11,11 @@ internal sealed class TangemPayState { data object Loading : TangemPayState() + data class OnboardingBanner( + val onClick: () -> Unit, + val closeOnClick: () -> Unit, + ) : TangemPayState() + data class Progress( val title: TextReference, val description: TextReference, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt new file mode 100644 index 0000000000..9e4e6bafd7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayHideOnboardingStateTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy(tangemPayState = TangemPayState.Empty) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt new file mode 100644 index 0000000000..15b2ad4dee --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayOnboardingBannerStateTransformer( + userWalletId: UserWalletId, + private val onClick: (UserWalletId) -> Unit, + private val closeOnClick: (UserWalletId) -> Unit, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy( + tangemPayState = TangemPayState.OnboardingBanner( + onClick = { onClick(userWalletId) }, + closeOnClick = { closeOnClick(userWalletId) }, + ), + ) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 6f182566d8..2f3bad4547 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.common.routing.AppRoute import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.MainCustomerInfoContentState @@ -79,6 +80,13 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( updateTangemPay(data = state.info, userWalletId = userWalletId) analytics.send(customerInfo = state.info) } + is MainCustomerInfoContentState.OnboardingBanner -> stateController.update( + transformer = TangemPayOnboardingBannerStateTransformer( + userWalletId = userWalletId, + onClick = clickIntents::onOnboardingBannerClick, + closeOnClick = clickIntents::onOnboardingBannerCloseClick, + ), + ) } } @@ -91,7 +99,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( userWalletId = userWalletId, value = data, cardFrozenState = cardFrozenState, - onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) }, + onClickKyc = { + innerWalletRouter.openTangemPayOnboarding( + mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId), + ) + }, onIssuingCard = clickIntents::onIssuingCardClicked, onIssuingFailed = clickIntents::onIssuingFailedClicked, openDetails = { config -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index 24f11c043c..7ee74d5ec2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -23,6 +23,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier) is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier) is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier) + is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier) TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt new file mode 100644 index 0000000000..b57061cb36 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState + +private const val GRADIENT_START_COLOR = 0xFF252934 +private const val GRADIENT_END_COLOR = 0xFF12141E +private const val GRADIENT_OFFSET_X = 0f +private const val GRADIENT_OFFSET_Y = 80F +private const val GRADIENT_RADIUS = 200F + +@Composable +internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background( + brush = Brush.radialGradient( + colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)), + center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y), + radius = GRADIENT_RADIUS, + ), + ) + .clickable(onClick = state.onClick), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner), + contentDescription = null, + modifier = Modifier + .padding(horizontal = 8.dp) + .width(55.dp) + .height(95.dp) + .offset(y = 10.dp), + ) + + SpacerH4() + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_onboarding_banner_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.constantWhite, + ) + + SpacerH(6.dp) + + Text( + text = stringResourceSafe(R.string.tangempay_onboarding_banner_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.TopEnd, + ) { + Image( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = state.closeOnClick) + .padding(4.dp) + .padding(top = 12.dp) + .size(12.dp), + painter = painterResource(id = R.drawable.ic_close_24), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + contentDescription = null, + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewTangemOnboardingBanner() { + TangemThemePreview { + TangemPayOnboardingBanner( + TangemPayState.OnboardingBanner( + onClick = {}, + closeOnClick = {}, + ), + ) + } +} \ No newline at end of file From a02b13ae6a42c6e41b755c5f310e9f4a3eeb803d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 13:11:59 +0100 Subject: [PATCH 075/130] Updated on 2026-08-14 --- .../ui/markets/MarketChartLoadingError.kt | 39 ---- .../news/trending/DefaultTrendingNewsStore.kt | 12 +- .../local/news/trending/TrendingNewsStore.kt | 8 +- .../news/repository/DefaultNewsRepository.kt | 77 +++++-- .../tangem/domain/models/news/NewsError.kt | 19 ++ .../tangem/domain/models/news/TrendingNews.kt | 16 ++ .../domain/news/repository/NewsRepository.kt | 15 +- .../news/usecase/FetchTrendingNewsUseCase.kt | 2 +- .../news/usecase/ManageTrendingNewsUseCase.kt | 18 +- .../components/DefaultFeedEntryComponent.kt | 54 +++-- .../feed/components/FeedEntryChildFactory.kt | 12 +- .../components/feed/DefaultFeedComponent.kt | 18 +- .../feed/model/feed/FeedComponentModel.kt | 127 ++++++++--- .../feed/model/feed/FeedModelClickIntents.kt | 15 ++ .../tangem/features/feed/ui/feed/FeedList.kt | 199 +++++++++++++----- .../features/feed/ui/feed/FeedListLoading.kt | 42 +++- .../features/feed/ui/feed/state/FeedListUM.kt | 11 +- .../feed/state/FeedMarketsBatchFlowManager.kt | 13 ++ .../ui/feed/state/TrendingNewsStateFactory.kt | 120 ++++++----- 19 files changed, 544 insertions(+), 273 deletions(-) delete mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt deleted file mode 100644 index 2cbcf9ad7c..0000000000 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketChartLoadingError.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.common.ui.markets - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme - -@Composable -fun MarketChartLoadingError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(vertical = 32.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResourceSafe(R.string.common_unable_to_load), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.tertiary, - ) - SpacerH(12.dp) - SecondarySmallButton( - config = SmallButtonConfig( - text = TextReference.Res(R.string.try_to_load_data_again_button_title), - onClick = onRetryClick, - ), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt index 23c1cad1db..157a5430e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt @@ -1,25 +1,25 @@ package com.tangem.datasource.local.news.trending import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -private typealias TrendingCache = Map> +private typealias TrendingCache = Map internal class DefaultTrendingNewsStore( private val store: RuntimeSharedStore, ) : TrendingNewsStore { - override fun get(key: String): Flow> { - return store.get().map { it[key].orEmpty() } + override fun get(key: String): Flow { + return store.get().map { it[key] ?: TrendingNews.Data(emptyList()) } } - override suspend fun getSyncOrNull(key: String): List? { + override suspend fun getSyncOrNull(key: String): TrendingNews? { return store.getSyncOrNull()?.get(key) } - override suspend fun store(key: String, value: List) { + override suspend fun store(key: String, value: TrendingNews) { store.update(emptyMap()) { current -> current + (key to value) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt index 24afaabda0..643ad55ee6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt @@ -1,15 +1,15 @@ package com.tangem.datasource.local.news.trending -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import kotlinx.coroutines.flow.Flow interface TrendingNewsStore { - fun get(key: String): Flow> + fun get(key: String): Flow - suspend fun getSyncOrNull(key: String): List? + suspend fun getSyncOrNull(key: String): TrendingNews? - suspend fun store(key: String, value: List) + suspend fun store(key: String, value: TrendingNews) suspend fun clear() } \ No newline at end of file diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index 426bc6f287..09cc6015b6 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -1,15 +1,16 @@ package com.tangem.data.news.repository +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.news.NewsApi +import com.tangem.datasource.api.news.models.response.NewsTrendingResponse import com.tangem.datasource.local.news.details.NewsDetailsStore import com.tangem.datasource.local.news.trending.TrendingNewsStore +import com.tangem.domain.models.news.* import com.tangem.domain.news.model.NewsListBatchFlow import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.news.model.NewsListConfig -import com.tangem.domain.models.news.ArticleCategory -import com.tangem.domain.models.news.DetailedArticle -import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.news.repository.NewsRepository import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListSource @@ -24,7 +25,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import kotlin.collections.orEmpty +import timber.log.Timber /** * Implementation of [NewsRepository]. @@ -67,26 +68,26 @@ internal class DefaultNewsRepository( fetchDetailedArticlesInternal(newsIds = newsIds, language = language) } - override suspend fun getTrendingNews(limit: Int, language: String?) { + override suspend fun fetchTrendingNews(limit: Int, language: String?) { fetchAndStoreTrendingNews(limit = limit, language = language) } - override fun observeTrendingNews(): Flow> { + override fun observeTrendingNews(): Flow { return trendingNewsStore.get(TRENDING_NEWS_KEY) } - override suspend fun refreshTrendingNews(limit: Int, language: String?) { - fetchAndStoreTrendingNews(limit = limit, language = language) - } - override suspend fun updateTrendingNewsViewed(articleIds: Collection, viewed: Boolean) { if (articleIds.isEmpty()) return - val current = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty() - if (current.isEmpty()) return + val currentResult = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) ?: return + val currentArticles = when (currentResult) { + is TrendingNews.Data -> currentResult.articles + is TrendingNews.Error -> return + } + if (currentArticles.isEmpty()) return val ids = articleIds.toSet() - val updated = current.map { article -> + val updated = currentArticles.map { article -> if (article.id in ids) { article.copy(viewed = viewed) } else { @@ -94,7 +95,7 @@ internal class DefaultNewsRepository( } } - trendingNewsStore.store(TRENDING_NEWS_KEY, updated) + trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(updated)) } override suspend fun getCategories(): List { @@ -137,16 +138,46 @@ internal class DefaultNewsRepository( } } - private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?): List { + private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) { return withContext(dispatchers.io) { - val response = newsApi.getTrendingNews(limit = limit, language = language).getOrThrow() - val freshArticles = response.items.map { it.toDomainShortArticle() } - val currentArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty() - val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit) - - trendingNewsStore.store(TRENDING_NEWS_KEY, merged) - - merged + val apiResponse = newsApi.getTrendingNews(limit = limit, language = language) + when (val result = apiResponse) { + is ApiResponse.Error -> { + Timber.e( + result.cause.cause, + "Trending news fetch failed cause: ${ + when (val error = result.cause) { + is ApiResponseError.HttpException -> error.code + is ApiResponseError.NetworkException -> "NetworkException" + is ApiResponseError.TimeoutException -> "TimeoutException" + is ApiResponseError.UnknownException -> "UnknownException" + } + }", + ) + trendingNewsStore.clear() + trendingNewsStore.store( + key = TRENDING_NEWS_KEY, + value = TrendingNews.Error( + NewsError.Unknown( + message = result.cause.message, + code = null, + ), + ), + ) + } + is ApiResponse.Success -> { + val freshArticles = result.data.items.map { it.toDomainShortArticle() } + val cachedArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) + val currentArticles = when (cachedArticles) { + is TrendingNews.Data -> cachedArticles.articles + is TrendingNews.Error -> emptyList() + null -> emptyList() + } + val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit) + trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(merged)) + TrendingNews.Data(merged) + } + } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt new file mode 100644 index 0000000000..5055eacbbf --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.models.news + +import kotlinx.serialization.Serializable + +@Serializable +sealed class NewsError { + abstract val message: String? + abstract val code: Int? + + data class ArticleNotFound( + override val message: String?, + override val code: Int?, + ) : NewsError() + + data class Unknown( + override val message: String?, + override val code: Int?, + ) : NewsError() +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt new file mode 100644 index 0000000000..d3b0bbdf87 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.models.news + +import kotlinx.serialization.Serializable + +/** + * Represents the result of fetching news. + * Can contain either data or an error. + */ +@Serializable +sealed interface TrendingNews { + @Serializable + data class Data(val articles: List) : TrendingNews + + @Serializable + data class Error(val throwable: NewsError) : TrendingNews +} \ No newline at end of file diff --git a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt index 7db6669b0d..3b9e06ce82 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt @@ -1,10 +1,10 @@ package com.tangem.domain.news.repository -import com.tangem.domain.news.model.NewsListBatchFlow -import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.models.news.ArticleCategory import com.tangem.domain.models.news.DetailedArticle -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews +import com.tangem.domain.news.model.NewsListBatchFlow +import com.tangem.domain.news.model.NewsListBatchingContext import kotlinx.coroutines.flow.Flow /** @@ -43,17 +43,12 @@ interface NewsRepository { * @param limit * @param language current device locale */ - suspend fun getTrendingNews(limit: Int, language: String?) + suspend fun fetchTrendingNews(limit: Int, language: String?) /** * Observes trending news with runtime viewed flag support. */ - fun observeTrendingNews(): Flow> - - /** - * Refreshes trending news list and updates cache without overriding viewed status. - */ - suspend fun refreshTrendingNews(limit: Int, language: String?) + fun observeTrendingNews(): Flow /** * Updates viewed flag for provided trending articles. diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt index 319f111625..fc69c6ba03 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt @@ -11,7 +11,7 @@ import java.util.Locale class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) { suspend operator fun invoke(): Either = Either.catch { - newsRepository.getTrendingNews( + newsRepository.fetchTrendingNews( limit = LIMIT_FOR_TRENDING_NEWS, language = Locale.getDefault().language, ) diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt index 55d69e8875..6d76aaa9f6 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt @@ -1,8 +1,9 @@ package com.tangem.domain.news.usecase -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import com.tangem.domain.news.repository.NewsRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged /** * Exposes trending news as a cached flow and provides helpers to refresh or mark items as viewed. @@ -12,17 +13,12 @@ import kotlinx.coroutines.flow.Flow class ManageTrendingNewsUseCase(private val repository: NewsRepository) { /** - * Observes the current cached list of trending articles (max 10 items). + * Observes the current cached list of trending articles (max 10 items) or error. */ - operator fun invoke(): Flow> { - return repository.observeTrendingNews() - } - - /** - * Forces refresh from backend while preserving local `viewed` flags. - */ - suspend fun refresh(limit: Int, language: String?) { - repository.refreshTrendingNews(limit, language) + fun observeTrendingNews(): Flow { + return repository + .observeTrendingNews() + .distinctUntilChanged() } /** diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index d27de2ff81..dbcfde3de2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -22,7 +22,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.EntryBottomSheetContent +import com.tangem.features.feed.ui.market.state.SortByTypeUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -40,6 +42,36 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) + private val clickIntents = object : FeedEntryClickIntents { + override fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) { + innerRouter.push( + route = FeedEntryChildFactory.Child.TokenDetails( + params = DefaultMarketsTokenDetailsComponent.Params( + token = token, + appCurrency = appCurrency, + shouldShowPortfolio = true, + analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( + blockchain = null, + source = "Market", + ), + ), + ), + ) + } + + override fun onMarketOpenClick(sortBy: SortByTypeUM) { + innerRouter.push(FeedEntryChildFactory.Child.TokenList) + } + + override fun onArticleClick(articleId: Int) { + innerRouter.push(FeedEntryChildFactory.Child.NewsDetails) + } + + override fun onOpenAllNews() { + innerRouter.push(FeedEntryChildFactory.Child.NewsList) + } + } + private val stack: Value> = childStack( key = "main", source = stackNavigation, @@ -53,7 +85,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( componentContext = factoryContext, router = innerRouter, ), - onTokenClick = ::marketsListTokenSelected, + feedEntryClickIntents = clickIntents, ) }, ) @@ -76,22 +108,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( ) } - private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { - innerRouter.push( - route = FeedEntryChildFactory.Child.TokenDetails( - params = DefaultMarketsTokenDetailsComponent.Params( - token = token, - appCurrency = appCurrency, - shouldShowPortfolio = true, - analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), - ) - } - private fun onChildBack() { if (stack.value.active.configuration !is FeedEntryChildFactory.Child.Feed) { stackNavigation.popWhile { it != FeedEntryChildFactory.Child.Feed } @@ -102,4 +118,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( interface Factory : FeedEntryComponent.Factory { override fun create(context: AppComponentContext): DefaultFeedEntryComponent } -} \ No newline at end of file +} + +internal interface FeedEntryClickIntents : FeedModelClickIntents \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 6483e57566..081ec2ab4d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -4,8 +4,6 @@ import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularContentComponent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent @@ -44,7 +42,7 @@ internal class FeedEntryChildFactory @Inject constructor() { fun createChild( child: Child, appComponentContext: AppComponentContext, - onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, + feedEntryClickIntents: FeedEntryClickIntents, ): ComposableModularContentComponent { return when (child) { is Child.TokenDetails -> { @@ -56,7 +54,12 @@ internal class FeedEntryChildFactory @Inject constructor() { is Child.TokenList -> { DefaultMarketsTokenListComponent( appComponentContext = appComponentContext, - onTokenClick = onTokenClick, + onTokenClick = { token, appCurrency -> + feedEntryClickIntents.onMarketItemClick( + token, + appCurrency, + ) + }, ) } Child.NewsDetails -> { @@ -72,6 +75,7 @@ internal class FeedEntryChildFactory @Inject constructor() { Child.Feed -> { DefaultFeedComponent( appComponentContext = appComponentContext, + params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index 910542c05a..0b6ca24074 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -3,19 +3,22 @@ package com.tangem.features.feed.components.feed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.features.feed.model.feed.FeedComponentModel -import com.tangem.features.feed.ui.feed.FeedListContent +import com.tangem.features.feed.model.feed.FeedModelClickIntents +import com.tangem.features.feed.ui.feed.FeedList import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, + private val params: FeedParams, ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { - private val feedComponentModel = getOrCreateModel() + private val feedComponentModel = getOrCreateModel(params = params) @Composable override fun Title() { @@ -25,8 +28,15 @@ internal class DefaultFeedComponent( @Composable override fun Content(modifier: Modifier) { + LifecycleStartEffect(Unit) { + feedComponentModel.isVisibleOnScreen.value = true + onStopOrDispose { + feedComponentModel.isVisibleOnScreen.value = false + } + } + val state by feedComponentModel.state.collectAsStateWithLifecycle() - FeedListContent( + FeedList( modifier = modifier, state = state, ) @@ -34,4 +44,6 @@ internal class DefaultFeedComponent( @Composable override fun Footer() = Unit + + data class FeedParams(val feedClickIntents: FeedModelClickIntents) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 6467b4cc3c..ee02fa8424 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.DateTimeFormatters @@ -11,8 +12,10 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -37,10 +40,10 @@ internal class FeedComponentModel @Inject constructor( private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + paramsContainer: ParamsContainer, ) : Model() { - private val _state = MutableStateFlow(initialState()) - val state = _state.asStateFlow() + private val params = paramsContainer.require() private var quotesUpdateJob: Job? = null @@ -59,42 +62,50 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) + internal val state: StateFlow + field = MutableStateFlow(initialState()) + private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) { SearchBarStateFactory( - currentStateProvider = Provider { _state.value }, - onStateUpdate = { newState -> _state.update { newState } }, + currentStateProvider = Provider { state.value }, + onStateUpdate = { newState -> state.update { newState } }, ) } private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) { TrendingNewsStateFactory( - currentStateProvider = Provider { _state.value }, - onStateUpdate = { newState -> _state.update { newState } }, + currentStateProvider = Provider { state.value }, + onStateUpdate = { newState -> state.update { newState } }, ) } + val isVisibleOnScreen = MutableStateFlow(false) + init { + updateCallbacks() + modelScope.launch(dispatchers.default) { fetchTrendingNewsUseCase() - subscribeOnTrendingNews() - } - _state.update { feedListUM -> - feedListUM.copy( - searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), - feedListCallbacks = feedListUM.feedListCallbacks.copy( - onSortTypeClick = ::onSortTypeClick, - ), - ) } modelScope.launch(dispatchers.default) { combine( - marketsBatchFlowManager.itemsByOrder, - marketsBatchFlowManager.loadingStatesByOrder, - marketsBatchFlowManager.errorStatesByOrder, - ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder -> + flow = marketsBatchFlowManager.itemsByOrder, + flow2 = marketsBatchFlowManager.loadingStatesByOrder, + flow3 = marketsBatchFlowManager.errorStatesByOrder, + flow4 = manageTrendingNewsUseCase.observeTrendingNews(), + ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult -> updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder) - val currentSortType = _state.value.marketChartConfig.currentSortByType + trendingNewsStateFactory.updateTrendingNewsState( + result = trendingNewsResult, + onRetryClicked = { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase.invoke() + } + }, + ) + updateGlobalState() + val currentSortType = state.value.marketChartConfig.currentSortByType val items = itemsByOrder[currentSortType] val isLoading = loadingStatesByOrder[currentSortType] == true if (items != null && items.isNotEmpty() && !isLoading) { @@ -122,12 +133,6 @@ internal class FeedComponentModel @Inject constructor( } } - private suspend fun subscribeOnTrendingNews() { - manageTrendingNewsUseCase().collect { articles -> - trendingNewsStateFactory.updateTrendingNewsState(articles) - } - } - private fun initialState(): FeedListUM { return FeedListUM( currentDate = getCurrentDate(), @@ -151,11 +156,12 @@ internal class FeedComponentModel @Inject constructor( marketChartConfig = MarketChartConfig( marketCharts = buildMap { SortByTypeUM.entries.forEach { - put(it, MarketChartUM.LoadingError(onRetryClicked = marketsBatchFlowManager::reloadAll)) + put(it, MarketChartUM.Loading) } }.toPersistentHashMap(), currentSortByType = SortByTypeUM.Trending, ), + globalState = GlobalFeedState.Loading, ) } @@ -169,7 +175,7 @@ internal class FeedComponentModel @Inject constructor( loadingStatesByOrder: Map, errorStatesByOrder: Map, ) { - _state.update { currentState -> + state.update { currentState -> val newMarketCharts = buildMap { SortByTypeUM.entries.forEach { sortByType -> val items = itemsByOrder[sortByType] ?: persistentListOf() @@ -224,8 +230,41 @@ internal class FeedComponentModel @Inject constructor( } } + private fun updateGlobalState() { + state.update { currentState -> + val newsState = currentState.news + val marketCharts = currentState.marketChartConfig.marketCharts + + val isNewsLoading = newsState is NewsUM.Loading + val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading } + + val isNewsError = newsState is NewsUM.Error + val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError } + + val newGlobalState = when { + isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading + isNewsError && areAllChartsError -> GlobalFeedState.Error( + onRetryClicked = { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase.invoke() + marketsBatchFlowManager.reloadAll() + } + }, + ) + else -> GlobalFeedState.Content + } + + val currentGlobalState = currentState.globalState + if (currentGlobalState::class != newGlobalState::class) { + currentState.copy(globalState = newGlobalState) + } else { + currentState + } + } + } + private fun onSortTypeClick(sortByType: SortByTypeUM) { - _state.update { currentState -> + state.update { currentState -> val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> when (chart) { is MarketChartUM.Content -> { @@ -256,11 +295,41 @@ internal class FeedComponentModel @Inject constructor( quotesUpdateJob = modelScope.launch { while (true) { delay(DELAY_TO_FETCH_QUOTES) + isVisibleOnScreen.first { it } marketsBatchFlowManager.updateQuotes() } } } + private fun updateCallbacks() { + state.update { feedListUM -> + feedListUM.copy( + searchBar = state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + feedListCallbacks = feedListUM.feedListCallbacks.copy( + onSortTypeClick = ::onSortTypeClick, + onMarketItemClick = { item -> + val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) + if (tokenMarket != null) { + params.feedClickIntents.onMarketItemClick( + token = tokenMarket.toSerializableParam(), + appCurrency = currentAppCurrency.value, + ) + } + }, + onMarketOpenClick = { sortBy -> + params.feedClickIntents.onMarketOpenClick(sortBy) + }, + onArticleClick = { articleId -> + params.feedClickIntents.onArticleClick(articleId) + }, + onOpenAllNews = { + params.feedClickIntents.onOpenAllNews() + }, + ), + ) + } + } + private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt new file mode 100644 index 0000000000..7e518f9c26 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.model.feed + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.feed.ui.market.state.SortByTypeUM + +/** + * Callback interface for feed model navigation actions. + */ +internal interface FeedModelClickIntents { + fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) + fun onMarketOpenClick(sortBy: SortByTypeUM) + fun onArticleClick(articleId: Int) + fun onOpenAllNews() +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 6a6b6b2b3f..c35da92100 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -1,9 +1,6 @@ package com.tangem.features.feed.ui.feed -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith +import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -31,7 +28,6 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.tangem.common.ui.markets.MarketChartLoadingError import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.common.ui.markets.models.MarketsListItemUM @@ -40,6 +36,7 @@ import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.SecondarySmallButton @@ -74,77 +71,123 @@ internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modif } @Composable -internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { +internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + AnimatedContent( + modifier = modifier, + targetState = state.globalState, + ) { animatedState -> + when (animatedState) { + is GlobalFeedState.Loading -> { + FeeListLoading( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .drawBehind { drawRect(background) }, + ) + } + is GlobalFeedState.Error -> { + FeedListGlobalError( + onRetryClick = animatedState.onRetryClicked, + modifier = Modifier.drawBehind { drawRect(background) }, + ) + } + is GlobalFeedState.Content -> { + FeeListContent( + modifier = Modifier, + state = state, + ) + } + } + } +} + +@Composable +private fun FeeListContent(state: FeedListUM, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .drawBehind { drawRect(background) }, ) { - SpacerH(20.dp) - - Text( + Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - text = stringResourceSafe(R.string.feed_market_and_news), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - text = state.currentDate, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.tertiary, - ) + .fillMaxSize() + .padding(WindowInsets.navigationBars.asPaddingValues()), + ) { + SpacerH(20.dp) - SpacerH(32.dp) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResourceSafe(R.string.feed_market_and_news), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = state.currentDate, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.tertiary, + ) - state.marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { marketChartUM -> - MarketBlock( - marketChart = marketChartUM, + SpacerH(32.dp) + + AnimatedVisibility(state.marketChartConfig.marketCharts[SortByTypeUM.Rating] != null) { + val marketChart = remember(state.marketChartConfig.marketCharts[SortByTypeUM.Rating]) { + state.marketChartConfig.marketCharts[SortByTypeUM.Rating] + } + if (marketChart != null) { + MarketBlock( + marketChart = marketChart, + feedListCallbacks = state.feedListCallbacks, + ) + } + } + + NewsBlock( + news = state.news, + feedListCallbacks = state.feedListCallbacks, + trendingArticle = state.trendingArticle, + ) + + MarketPulseBlock( + marketChartConfig = state.marketChartConfig, feedListCallbacks = state.feedListCallbacks, ) } - - NewsBlock( - news = state.news, - feedListCallbacks = state.feedListCallbacks, - trendingArticle = state.trendingArticle, - ) - - MarketPulseBlock( - marketChartConfig = state.marketChartConfig, - feedListCallbacks = state.feedListCallbacks, - ) } } @Composable private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) { - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, - ) + Column(modifier = Modifier.fillMaxWidth()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + ) - SpacerH(12.dp) + SpacerH(12.dp) - Charts( - onItemClick = feedListCallbacks.onMarketItemClick, - modifier = Modifier.padding(horizontal = 16.dp), - marketChart = marketChart, - ) - SpacerH(32.dp) + Charts( + onItemClick = feedListCallbacks.onMarketItemClick, + modifier = Modifier.padding(horizontal = 16.dp), + marketChart = marketChart, + ) + + SpacerH(32.dp) + } } @Composable @@ -219,6 +262,9 @@ private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendi NewsUM.Loading -> { NewsLoadingBlock() } + is NewsUM.Error -> { + NewsErrorBlock(onRetryClick = newsUM.onRetryClicked) + } } } } @@ -342,9 +388,9 @@ private fun Charts( } } is MarketChartUM.LoadingError -> { - MarketChartLoadingError( - modifier = Modifier.fillMaxWidth(), + UnableToLoadData( onRetryClick = marketChart.onRetryClicked, + modifier = Modifier.fillMaxWidth(), ) } is MarketChartUM.Content -> { @@ -392,6 +438,43 @@ private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: } } +@Composable +private fun FeedListGlobalError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Box( + modifier = modifier + .fillMaxSize() + .drawBehind { drawRect(background) } + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onRetryClick) + } +} + +@Composable +private fun NewsErrorBlock(onRetryClick: () -> Unit) { + Column { + Header( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResourceSafe(R.string.common_news), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } + }, + onSeeAllClick = {}, + ) + SpacerH(12.dp) + UnableToLoadData( + onRetryClick = onRetryClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 private const val GRADIENT_START = 0f private const val GRADIENT_END = 0.5f @@ -402,6 +485,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED) @Composable private fun FeedListPreview() { TangemThemePreview { - FeedListContent(state = createFeedPreviewState()) + FeedList(state = createFeedPreviewState()) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt index 0d936738f5..2e24ff3ec2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -16,6 +16,21 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.res.TangemThemePreview +@Composable +internal fun FeeListLoading(modifier: Modifier = Modifier) { + Column(modifier) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(WindowInsets.navigationBars.asPaddingValues()), + ) { + MarketLoadingBlock() + NewsLoadingBlock() + MarketPulseLoadingBlock() + } + } +} + @Composable internal fun MarketLoadingBlock() { RectangleShimmer( @@ -23,15 +38,19 @@ internal fun MarketLoadingBlock() { .padding(start = 16.dp) .size(width = 104.dp, height = 18.dp), ) - SpacerH(12.dp) + SpacerH(15.dp) ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) - SpacerH(32.dp) + SpacerH(35.dp) } @Composable internal fun MarketPulseLoadingBlock() { - RectangleShimmer() - SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(15.dp) LazyRow( modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -43,7 +62,7 @@ internal fun MarketPulseLoadingBlock() { RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp)) } } - SpacerH(12.dp) + SpacerH(16.dp) ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) SpacerH(32.dp) } @@ -51,8 +70,12 @@ internal fun MarketPulseLoadingBlock() { @Composable internal fun NewsLoadingBlock() { Column { - RectangleShimmer() - SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(15.dp) TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp)) SpacerH(12.dp) LazyRow( @@ -65,6 +88,7 @@ internal fun NewsLoadingBlock() { DefaultLoadingArticle() } } + SpacerH(35.dp) } } @@ -90,10 +114,8 @@ private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 private fun FeedListLoadingPreview() { TangemThemePreview { Column { - NewsLoadingBlock() - SpacerH(10.dp) MarketLoadingBlock() - SpacerH(10.dp) + NewsLoadingBlock() MarketPulseLoadingBlock() } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 519472ae49..3b613ccc86 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -16,6 +16,7 @@ internal data class FeedListUM( val news: NewsUM, val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, + val globalState: GlobalFeedState = GlobalFeedState.Content, ) internal data class FeedListCallbacks( @@ -31,6 +32,7 @@ internal data class FeedListCallbacks( internal sealed interface NewsUM { data object Loading : NewsUM data class Content(val content: ImmutableList) : NewsUM + data class Error(val onRetryClicked: () -> Unit) : NewsUM } internal data class MarketChartConfig( @@ -56,4 +58,11 @@ internal sealed interface MarketChartUM { internal data class SortChartConfigUM( val sortByType: SortByTypeUM, val isSelected: Boolean, -) \ No newline at end of file +) + +@Immutable +internal sealed interface GlobalFeedState { + data object Loading : GlobalFeedState + data object Content : GlobalFeedState + data class Error(val onRetryClicked: () -> Unit) : GlobalFeedState +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt index bdda39948d..7fa17ca9f5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.feed.state import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.model.converter.MarketsTokenItemConverter import com.tangem.features.feed.ui.market.state.MarketsListUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -126,6 +127,11 @@ internal class FeedMarketsBatchFlowManager( return managersByOrder[order]?.onLastBatchLoadedSuccess } + fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? { + return managersByOrder.values + .firstNotNullOfOrNull { manager -> manager.getTokenMarketById(tokenId) } + } + private class SingleOrderManager( val order: TokenMarketListConfig.Order, private val actionsFlow: MutableSharedFlow>, @@ -359,6 +365,13 @@ internal class FeedMarketsBatchFlowManager( } } + fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? { + return resultBatches.value.processedItems + ?.asSequence() + ?.flatMap { it.data } + ?.firstOrNull { it.id == tokenId } + } + private data class ResultBatches( val uiBatches: List>> = emptyList(), val processedItems: List>>? = null, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt index f096546b37..15aee2ff6a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.utils.getFormattedDate import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import com.tangem.features.feed.impl.R import com.tangem.utils.Provider import com.tangem.utils.StringsSigns @@ -23,71 +24,78 @@ internal class TrendingNewsStateFactory( private val onStateUpdate: (FeedListUM) -> Unit, ) { - fun updateTrendingNewsState(news: List) { - val trendingArticleIndex = news.indexOfFirst { it.isTrending } - val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null - val commonArticles = if (trendingArticleIndex != -1) { - news.toMutableList().apply { removeAt(trendingArticleIndex) } - } else { - news - } + fun updateTrendingNewsState(result: TrendingNews, onRetryClicked: () -> Unit) { val currentState = currentStateProvider() + when (result) { + is TrendingNews.Data -> handleDataState(currentState, result.articles) + is TrendingNews.Error -> handleErrorState(currentState, onRetryClicked) + } + } + + private fun handleDataState(currentState: FeedListUM, articles: List) { + val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles) + onStateUpdate( currentState.copy( - trendingArticle = trendingArticle?.let { article -> - ArticleConfigUM( - id = article.id, - title = article.title, - score = article.score, - isTrending = true, - tags = article.categories.map { category -> - LabelUM(text = TextReference.Str(category.name)) - }.plus( - article.relatedTokens.map { token -> - LabelUM( - text = TextReference.Str(token.symbol), - leadingContent = LabelLeadingContentUM.Token( - iconUrl = getTokenIconUrlFromDefaultHost( - tokenId = CryptoCurrency.RawID(token.id), - ), - ), - ) - }, - ).toPersistentSet(), - createdAt = mapFormattedDate(article.createdAt), - isViewed = article.viewed, - ) - }, + trendingArticle = trendingArticle?.let { mapToArticleConfigUM(it, isTrending = true) }, news = NewsUM.Content( - commonArticles.map { article -> - ArticleConfigUM( - id = article.id, - title = article.title, - score = article.score, - isTrending = false, - tags = article.categories.map { category -> - LabelUM(text = TextReference.Str(category.name)) - }.plus( - article.relatedTokens.map { token -> - LabelUM( - text = TextReference.Str(token.symbol), - leadingContent = LabelLeadingContentUM.Token( - iconUrl = getTokenIconUrlFromDefaultHost( - tokenId = CryptoCurrency.RawID(token.id), - ), - ), - ) - }, - ).toPersistentSet(), - createdAt = mapFormattedDate(article.createdAt), - isViewed = article.viewed, - ) - }.toPersistentList(), + commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList(), ), ), ) } + private fun handleErrorState(currentState: FeedListUM, onRetryClicked: () -> Unit) { + onStateUpdate( + currentState.copy( + trendingArticle = null, + news = NewsUM.Error(onRetryClicked = onRetryClicked), + ), + ) + } + + private fun separateTrendingAndCommonArticles( + articles: List, + ): Pair> { + val trendingArticleIndex = articles.indexOfFirst { it.isTrending } + return if (trendingArticleIndex != -1) { + val trendingArticle = articles[trendingArticleIndex] + val commonArticles = articles.toMutableList().apply { removeAt(trendingArticleIndex) } + trendingArticle to commonArticles + } else { + null to articles + } + } + + private fun mapToArticleConfigUM(article: ShortArticle, isTrending: Boolean): ArticleConfigUM { + return ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = isTrending, + tags = buildArticleTags(article), + createdAt = mapFormattedDate(article.createdAt), + isViewed = article.viewed, + ) + } + + private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet { + val categoryLabels = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + } + val tokenLabels = article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + } + return (categoryLabels + tokenLabels).toPersistentSet() + } + private fun mapFormattedDate(createdAt: String): TextReference { val formattedDate = getFormattedDate( createdAt = createdAt, From 0ccfceba0426fa257673a45f3342b9f94c65ce7b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 19:12:28 +0700 Subject: [PATCH 076/130] Updated on 2026-08-14 --- .../account/usecase/ApplyAccountListSortingUseCase.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt index 9c7d51c56f..62c8ea8096 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt @@ -70,9 +70,16 @@ class ApplyAccountListSortingUseCase( return@eitherOn } - accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + applySorting(accountList = updatedAccountList) } + private suspend fun applySorting(accountList: AccountList) { + catch( + block = { accountsCRUDRepository.saveAccounts(accountList) }, + catch = { accountsCRUDRepository.saveAccountsLocally(accountList) }, + ) + } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { accountsCRUDRepository.getAccountListSync(userWalletId = userWalletId) }, From 35e0cccc90fff00db313bca50b8407b6fe5a9937 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 15:12:42 +0300 Subject: [PATCH 077/130] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 2 + .../DefaultOnrampTransactionRepository.kt | 65 +++++++++++++++++++ .../models/OnrampTerminalTransactionDTO.kt | 18 +++++ .../domain/onramp/model/error/OnrampError.kt | 2 + .../onramp/GetOnrampTransactionUseCase.kt | 5 ++ .../onramp/OnrampRemoveTransactionUseCase.kt | 5 +- .../OnrampTransactionRepository.kt | 4 ++ .../alloffers/entity/AllOffersStateFactory.kt | 1 + .../main/entity/factory/OnrampStateFactory.kt | 1 + .../entity/factory/OnrampV2StateFactory.kt | 1 + .../model/OnrampSuccessComponentModel.kt | 10 +-- .../utils/OnrampErrorAnalyticsSender.kt | 1 + 12 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 775e916143..3fafa4462a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -108,6 +108,8 @@ object PreferencesKeys { val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") } + val ONRAMP_HANDLED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "onrampHandledTransactions") } + val ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY by lazy { stringPreferencesKey(name = "onboardingFinalizeScanResponse") } val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index 5e515100e3..782b4b7b5b 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.onramp import com.tangem.data.onramp.converters.TransactionConverter +import com.tangem.data.onramp.models.OnrampTerminalTransactionDTO import com.tangem.data.onramp.models.OnrampTransactionDTO import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -15,7 +16,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit internal class DefaultOnrampTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -47,6 +50,7 @@ internal class DefaultOnrampTransactionRepository( override fun getAllTransactions(): Flow> { return appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.map(transactionConverter::convert) } } @@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository( cryptoCurrencyId: CryptoCurrency.ID, ): Flow> = appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.filter { it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value @@ -101,4 +106,64 @@ internal class DefaultOnrampTransactionRepository( } } } + + override suspend fun isHandledTransaction(txId: String): Boolean = withContext(dispatchers.io) { + val terminated = appPreferencesStore.getObjectSetSync( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ) + + terminated.any { it.txId == txId } + } + + override suspend fun storeHandledTransaction(txId: String) { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ).orEmpty().toMutableSet() + + val record = OnrampTerminalTransactionDTO( + txId = txId, + terminatedAt = System.currentTimeMillis(), + ) + + archived.add(record) + + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = archived, + ) + } + } + } + } + + private suspend fun cleanupExpiredTerminalTransactions() { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + )?.toMutableSet() ?: return@editData + + val oneWeekAgo = System.currentTimeMillis() - + TimeUnit.DAYS.toMillis(HANDLED_TRANSACTIONS_RETENTION_DAYS) + val cleaned = archived + .filterTo(mutableSetOf()) { it.terminatedAt > oneWeekAgo } + + if (cleaned.size != archived.size) { + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = cleaned, + ) + } + } + } + } + } + + private companion object { + const val HANDLED_TRANSACTIONS_RETENTION_DAYS = 7L + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt new file mode 100644 index 0000000000..46aae3c528 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt @@ -0,0 +1,18 @@ +package com.tangem.data.onramp.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Model for terminal onramp transaction + * + * @property txId Transaction ID + * @property terminatedAt Termination timestamp in milliseconds + */ +@JsonClass(generateAdapter = true) +data class OnrampTerminalTransactionDTO( + @Json(name = "txId") + val txId: String, + @Json(name = "terminatedAt") + val terminatedAt: Long, +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt index cf1f656e93..96e2f710ba 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt @@ -31,4 +31,6 @@ sealed class OnrampError { ) : OnrampError() data object PairsNotFound : OnrampError() + + data object AlreadyHandledTransaction : OnrampError() } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt index 72ab25fe7c..2ac28eb227 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.onramp import arrow.core.Either +import arrow.core.left import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver @@ -12,6 +13,10 @@ class GetOnrampTransactionUseCase( ) { suspend operator fun invoke(txId: String): Either { + if (onrampTransactionRepository.isHandledTransaction(txId)) { + return OnrampError.AlreadyHandledTransaction.left() + } + return Either.catch { requireNotNull( onrampTransactionRepository.getTransactionById(txId), diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt index 3d94a2562c..2eddae74ac 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt @@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String?): Either { + suspend operator fun invoke(txId: String?, forceRemove: Boolean = false): Either { if (txId == null) return OnrampError.DomainError("Transaction id not provided").left() return Either.catch { + if (!forceRemove) { + onrampTransactionRepository.storeHandledTransaction(txId) + } onrampTransactionRepository.removeTransaction(txId) }.mapLeft(errorResolver::resolve) } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt index c0ab3000ce..46cfdcbe6f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt @@ -24,4 +24,8 @@ interface OnrampTransactionRepository { ) suspend fun removeTransaction(txId: String) + + suspend fun storeHandledTransaction(txId: String) + + suspend fun isHandledTransaction(txId: String): Boolean } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 4a29045354..3dcc789481 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -111,6 +111,7 @@ internal class AllOffersStateFactory( -> true is OnrampError.AmountError, is OnrampError.RedirectError, + is OnrampError.AlreadyHandledTransaction, -> false } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index b0a9d7773a..8faf435657 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -63,6 +63,7 @@ internal class OnrampStateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt index ad9a4c4577..06ec867696 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt @@ -86,6 +86,7 @@ internal class OnrampV2StateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 5f318b6a4f..15b4eed93a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -177,10 +177,12 @@ internal class OnrampSuccessComponentModel @Inject constructor( private fun showErrorAlert(error: OnrampError) { val errorCode = (error as? OnrampError.DataError)?.code val message = DialogMessage( - message = if (errorCode.isNullOrBlank()) { - resourceReference(R.string.common_unknown_error) - } else { - resourceReference(R.string.express_error_code, wrappedList(errorCode)) + message = when { + !errorCode.isNullOrBlank() -> resourceReference(R.string.express_error_code, wrappedList(errorCode)) + error is OnrampError.AlreadyHandledTransaction -> resourceReference( + R.string.onramp_error_transaction_already_processed, + ) + else -> resourceReference(R.string.common_unknown_error) }, firstActionBuilder = { okAction(router::pop) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt index e9ec5c76c5..f39254139d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt @@ -31,5 +31,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent( OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, OnrampError.PairsNotFound, + OnrampError.AlreadyHandledTransaction, -> { /* no-op */ } } \ No newline at end of file From 495545fb5816a4ad121b4a7412ea4d230981451d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 16:13:18 +0400 Subject: [PATCH 078/130] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/model/SwapModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 4cdd1f15ff..3c9ba8dff3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -391,7 +391,7 @@ internal class SwapModel @Inject constructor( } else { initialFromStatus to selectedCurrency } - val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled) { + val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) { if (isOrderReversed) { selectedAccount to fromAccountCurrencyStatus.account } else { From 2cbaf822cfa355a3b6ab35ba933206fb60f818f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 16:22:06 +0400 Subject: [PATCH 079/130] Updated on 2026-08-14 --- ...mPayGenerateAddressAndSignChallengeTask.kt | 8 +- .../datasource/api/common/config/TangemPay.kt | 4 +- .../tangem/datasource/api/pay/TangemPayApi.kt | 98 +---------------- .../pay/models/response/CustomerMeResponse.kt | 36 +++--- ...rResponse.kt => TangemPayErrorResponse.kt} | 2 +- .../com/tangem/datasource/api/visa/VisaApi.kt | 104 ++++++++++++++++++ .../request/ActivationByCardWalletRequest.kt | 2 +- .../ActivationByCustomerWalletRequest.kt | 2 +- .../models/request/ActivationStatusRequest.kt | 2 +- .../request/ExchangeAccessTokenRequest.kt | 2 +- .../request/GenerateNoneByCardIdRequest.kt | 2 +- .../GenerateNoneByCardWalletRequest.kt | 2 +- .../request/GetAccessTokenByCardIdRequest.kt | 2 +- .../GetAccessTokenByCardWalletRequest.kt | 2 +- .../request/GetCardWalletAcceptanceRequest.kt | 2 +- .../GetCustomerWalletAcceptanceRequest.kt | 2 +- .../CardActivationRemoteStateResponse.kt | 2 +- .../models/response/GenerateNonceResponse.kt | 2 +- .../models/response/JWTResponse.kt | 2 +- .../models/response/VisaCustomerInfo.kt | 2 +- .../models/response/VisaDataToSignResponse.kt | 2 +- .../models/response/VisaTxHistoryResponse.kt | 2 +- .../com/tangem/datasource/di/NetworkModule.kt | 13 ++- .../managers/ProdApiConfigsManagerTest.kt | 2 +- .../data/pay/util/TangemPayErrorConverter.kt | 6 +- .../visa/DefaultTangemPayRemoteDataSource.kt | 87 +++++++++++++++ .../visa/DefaultVisaActivationRepository.kt | 17 +-- .../visa/DefaultVisaAuthRemoteDataSource.kt | 77 +++---------- .../tangem/data/visa/DefaultVisaRepository.kt | 6 +- .../VisaActivationStatusConverterWithState.kt | 2 +- .../com/tangem/data/visa/di/VisaDataModule.kt | 16 +-- .../data/visa/utils/VisaApiRequestMaker.kt | 4 +- .../data/visa/utils/VisaTxDetailsFactory.kt | 2 +- .../visa/utils/VisaTxHistoryItemConverter.kt | 2 +- .../visa/utils/VisaTxHistoryPagingSource.kt | 2 +- .../datasource/TangemPayRemoteDataSource.kt | 20 ++++ .../datasource/VisaAuthRemoteDataSource.kt | 12 -- 37 files changed, 310 insertions(+), 242 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/{VisaErrorResponse.kt => TangemPayErrorResponse.kt} (90%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/ActivationByCardWalletRequest.kt (95%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/ActivationByCustomerWalletRequest.kt (90%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/ActivationStatusRequest.kt (82%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/ExchangeAccessTokenRequest.kt (82%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GenerateNoneByCardIdRequest.kt (85%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GenerateNoneByCardWalletRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GetAccessTokenByCardIdRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GetAccessTokenByCardWalletRequest.kt (87%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GetCardWalletAcceptanceRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/request/GetCustomerWalletAcceptanceRequest.kt (87%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/CardActivationRemoteStateResponse.kt (93%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/GenerateNonceResponse.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/JWTResponse.kt (93%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/VisaCustomerInfo.kt (89%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/VisaDataToSignResponse.kt (84%) rename core/datasource/src/main/java/com/tangem/datasource/api/{pay => visa}/models/response/VisaTxHistoryResponse.kt (98%) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt index 5c813c1a93..bc05e53d63 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -11,7 +11,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet @@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, private val dispatchersProvider: CoroutineDispatcherProvider, - private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, + private val tangemPayRemoteDataSource: TangemPayRemoteDataSource, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { @@ -52,7 +52,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey) val challenge = withContext(dispatchersProvider.io) { - visaAuthRemoteDataSource.getCustomerWalletAuthChallenge( + tangemPayRemoteDataSource.getCustomerWalletAuthChallenge( customerWalletAddress = address, customerWalletId = userWalletId.stringValue, ) @@ -71,7 +71,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( } val authTokens = withContext(dispatchersProvider.io) { - visaAuthRemoteDataSource.getTokenWithCustomerWallet( + tangemPayRemoteDataSource.getTokenWithCustomerWallet( sessionId = challenge.session.sessionId, signature = signedData.signature, nonce = signedData.dataToSign.hashToSign, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index 30a3300678..c0a5c07775 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -31,7 +31,7 @@ internal class TangemPay( private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://api.dev.us.paera.com/bff/", + baseUrl = "https://api.dev.us.paera.com/bff-v2/", headers = createHeaders(), ) @@ -43,7 +43,7 @@ internal class TangemPay( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.us.paera.com/bff/", + baseUrl = "https://api.us.paera.com/bff-v2/", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 57d31d6a0d..07719ff953 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -3,109 +3,13 @@ package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.POST -import retrofit2.http.PUT -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20 @Suppress("TooManyFunctions") interface TangemPayApi { - // region: auth - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardWallet( - @Body request: GenerateNoneByCardWalletRequest, - ): ApiResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse - - @POST("v1/auth/token") - suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCustomerWalletAccessToken( - @Body request: RefreshCustomerWalletAccessTokenRequest, - ): ApiResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse - - @POST("v1/auth/token/exchange") - suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse - - // endregion - - // region: activation - - @POST("v1/activation/status") - suspend fun getRemoteActivationStatus( - @Header("Authorization") authHeader: String, - @Body request: ActivationStatusRequest, - ): ApiResponse - - @POST("v1/activation/acceptance/message") - suspend fun getCardWalletAcceptance( - @Header("Authorization") authHeader: String, - @Body request: GetCardWalletAcceptanceRequest, - ): ApiResponse - - @POST("v1/activation/acceptance/message") - suspend fun getCustomerWalletAcceptance( - @Header("Authorization") authHeader: String, - @Body request: GetCustomerWalletAcceptanceRequest, - ): ApiResponse - - @POST("v1/activation/data") - suspend fun activateByCardWallet( - @Header("Authorization") authHeader: String, - @Body body: ActivationByCardWalletRequest, - ): ApiResponse - - @POST("v1/activation/data") - suspend fun activateByCustomerWallet( - @Header("Authorization") authHeader: String, - @Body body: ActivationByCustomerWalletRequest, - ): ApiResponse - - @POST("v1/activation/pin") - suspend fun setPinCode( - @Header("Authorization") authHeader: String, - @Body body: SetPinCodeRequest, - ): ApiResponse - - // endregion - - @GET("customer/info") - suspend fun getCustomerInfo( - @Header("Authorization") authHeader: String, - @Query("card_id") cardId: String, - ): ApiResponse - - @GET("product_instance/transactions") - suspend fun getTxHistory( - @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("limit") limit: Int, - @Query("offset") offset: Int, - ): ApiResponse - @GET("v1/customer/transactions") suspend fun getTangemPayTxHistory( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index fd89823bea..baf6d5d265 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -12,11 +12,11 @@ data class CustomerMeResponse( data class Result( @Json(name = "id") val id: String, @Json(name = "state") val state: String, - @Json(name = "createdAt") val createdAt: String, + @Json(name = "created_at") val createdAt: String, @Json(name = "product_instance") val productInstance: ProductInstance?, @Json(name = "payment_account") val paymentAccount: PaymentAccount?, @Json(name = "kyc") val kyc: Kyc?, - @Json(name = "depositAddress") val depositAddress: String?, + @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "card") val card: Card?, @Json(name = "balance") val balance: BalanceResponse?, ) @@ -24,49 +24,49 @@ data class CustomerMeResponse( @JsonClass(generateAdapter = true) data class ProductInstance( @Json(name = "id") val id: String, - @Json(name = "cid") val cid: String, + @Json(name = "cid") val cid: String?, @Json(name = "card_id") val cardId: String, - @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String?, @Json(name = "status") val status: Status, @Json(name = "updated_at") val updatedAt: String, @Json(name = "payment_account_id") val paymentAccountId: String, ) { @JsonClass(generateAdapter = false) enum class Status { - @Json(name = "new") + @Json(name = "NEW") NEW, - @Json(name = "ready_for_manufacturing") + @Json(name = "READY_FOR_MANUFACTURING") READY_FOR_MANUFACTURING, - @Json(name = "manufacturing") + @Json(name = "MANUFACTURING") MANUFACTURING, - @Json(name = "sent_to_delivery") + @Json(name = "SENT_TO_DELIVERY") SENT_TO_DELIVERY, - @Json(name = "delivered") + @Json(name = "DELIVERED") DELIVERED, - @Json(name = "activating") + @Json(name = "ACTIVATING") ACTIVATING, - @Json(name = "active") + @Json(name = "ACTIVE") ACTIVE, - @Json(name = "blocked") + @Json(name = "BLOCKED") BLOCKED, - @Json(name = "deactivating") + @Json(name = "DEACTIVATING") DEACTIVATING, - @Json(name = "deactivated") + @Json(name = "DEACTIVATED") DEACTIVATED, - @Json(name = "canceled") + @Json(name = "CANCELED") CANCELED, - @Json(name = "unknown") + @Json(name = "UNKNOWN") UNKNOWN, } } @@ -91,8 +91,8 @@ data class CustomerMeResponse( @JsonClass(generateAdapter = true) data class Card( @Json(name = "token") val token: String, - @Json(name = "expiration_month") val expirationMonth: Int, - @Json(name = "expiration_year") val expirationYear: Int, + @Json(name = "expiration_month") val expirationMonth: String, + @Json(name = "expiration_year") val expirationYear: String, @Json(name = "emboss_name") val embossName: String, @Json(name = "card_type") val cardType: String, @Json(name = "card_status") val cardStatus: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt index b211405000..7ae65cd653 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class VisaErrorResponse( +data class TangemPayErrorResponse( @Json(name = "error") val error: Error, ) { @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt new file mode 100644 index 0000000000..cda093a821 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt @@ -0,0 +1,104 @@ +package com.tangem.datasource.api.visa + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest +import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse +import com.tangem.datasource.api.visa.models.response.JWTResponse +import com.tangem.datasource.api.visa.models.response.VisaCustomerInfo +import com.tangem.datasource.api.visa.models.response.VisaDataToSignResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query + +interface VisaApi { + + @POST("v1/auth/token/refresh") + suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardWallet( + @Body request: GenerateNoneByCardWalletRequest, + ): ApiResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token/exchange") + suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse + + @POST("v1/activation/status") + suspend fun getRemoteActivationStatus( + @Header("Authorization") authHeader: String, + @Body request: ActivationStatusRequest, + ): ApiResponse + + @POST("v1/activation/acceptance/message") + suspend fun getCardWalletAcceptance( + @Header("Authorization") authHeader: String, + @Body request: GetCardWalletAcceptanceRequest, + ): ApiResponse + + @POST("v1/activation/acceptance/message") + suspend fun getCustomerWalletAcceptance( + @Header("Authorization") authHeader: String, + @Body request: GetCustomerWalletAcceptanceRequest, + ): ApiResponse + + @POST("v1/activation/data") + suspend fun activateByCardWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCardWalletRequest, + ): ApiResponse + + @POST("v1/activation/data") + suspend fun activateByCustomerWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCustomerWalletRequest, + ): ApiResponse + + @POST("v1/activation/pin") + suspend fun setPinCode( + @Header("Authorization") authHeader: String, + @Body body: SetPinCodeRequest, + ): ApiResponse + + @GET("customer/info") + suspend fun getCustomerInfo( + @Header("Authorization") authHeader: String, + @Query("card_id") cardId: String, + ): ApiResponse + + @GET("product_instance/transactions") + suspend fun getTxHistory( + @Header("Authorization") authHeader: String, + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("limit") limit: Int, + @Query("offset") offset: Int, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt index a5dafd7b32..c16198f0c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt index 731188d92a..ace2090603 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt index 9f3e017e55..7ce0614abc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt index 164d71fa5c..63b9fe43d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt index 7ab945b8f8..d3aa5eaa50 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt index 95d2f8f9bf..69c2f7a7d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt index 380284bab7..c212c81412 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt index 50fc683c9b..df5d0c3935 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt index de4cec941b..dd084fa326 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt index 7ff5c16337..3948fc43b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt index ae9c701b06..a34a31a366 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt index 958580bc5e..9a79a457b8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt index c28a882ae9..0380074d02 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt index b6a227f62f..70d5f6ac94 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt similarity index 84% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt index a35c99ab59..a19d3e7ca4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt similarity index 98% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt index aea17cc4c1..7c4d7d0a22 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 6c07212416..511a2dab3d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfigs -import com.tangem.datasource.api.common.config.MoonPay import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager @@ -21,6 +20,7 @@ import com.tangem.datasource.api.pay.TangemPayAuthApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.YieldSupplyApi +import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.di.utils.RetrofitApiBuilder import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -129,7 +129,16 @@ internal object NetworkModule { @Provides @Singleton - fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + fun provideTangemPayApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemPay, + applyTimeoutAnnotations = false, + ) + } + + @Provides + @Singleton + fun provideVisaApi(retrofitApiBuilder: RetrofitApiBuilder): VisaApi { return retrofitApiBuilder.build( apiConfigId = ApiConfig.ID.TangemPay, applyTimeoutAnnotations = false, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index bf6b755bc5..e176d0b768 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -251,7 +251,7 @@ internal class ProdApiConfigsManagerTest { id = ApiConfig.ID.TangemPay, expected = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://api.dev.us.paera.com/bff/", + baseUrl = "https://api.dev.us.paera.com/bff-v2/", headers = mapOf( "version" to ProviderSuspend { VERSION_NAME }, "platform" to ProviderSuspend { "Android" }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index 8080f3b642..1b81ab80eb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.pay.util import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.pay.models.response.VisaErrorResponse +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.visa.error.VisaApiError import com.tangem.utils.converter.Converter @@ -14,7 +14,7 @@ internal class TangemPayErrorConverter @Inject constructor( @NetworkMoshi moshi: Moshi, ) : Converter { - private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) } + private val tangemPayErrorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override fun convert(value: Throwable): VisaApiError { return if (value is ApiResponseError.HttpException) { @@ -23,7 +23,7 @@ internal class TangemPayErrorConverter @Inject constructor( val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode return runCatching { - visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode + tangemPayErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode }.map { VisaApiError.fromBackendError(it) }.getOrElse { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt new file mode 100644 index 0000000000..c2a927964b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt @@ -0,0 +1,87 @@ +package com.tangem.data.visa + +import arrow.core.Either +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.pay.TangemPayAuthApi +import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayAuthTokens +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSession +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultTangemPayRemoteDataSource @Inject constructor( + @NetworkMoshi private val moshi: Moshi, + private val tangemPayAuthApi: TangemPayAuthApi, + private val dispatchers: CoroutineDispatcherProvider, +) : TangemPayRemoteDataSource { + + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } + + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + customerWalletId: String, + ): Either = withContext(dispatchers.io) { + request { + tangemPayAuthApi.generateNonceByCustomerWallet( + request = GenerateNonceByCustomerWalletRequest( + customerWalletAddress = customerWalletAddress, + customerWalletId = customerWalletId, + ), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.nonce, + session = VisaAuthSession(response.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + tangemPayAuthApi.getTokenByCustomerWallet( + request = GetTokenByCustomerWalletRequest( + authType = "customer_wallet", + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + TangemPayAuthTokens( + accessToken = response.accessToken, + expiresAt = response.expiresAt, + refreshToken = response.refreshToken, + refreshExpiresAt = response.refreshExpiresAt, + ) + } + } + + private suspend fun request(requestBlock: suspend () -> T): Either { + return runCatching { + Either.Right(requestBlock()) + }.getOrElse { responseError -> + if (responseError is ApiResponseError.HttpException && + responseError.errorBody != null + ) { + val errorCode = + errorAdapter.fromJson(responseError.errorBody)?.error?.code ?: responseError.code.numericCode + return Either.Left(VisaApiError.fromBackendError(errorCode)) + } + + return Either.Left(VisaApiError.UnknownWithoutCode) + } + } +} \ No newline at end of file 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 5b5ee71abe..0b617bfe36 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 @@ -10,15 +10,16 @@ import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.request.* -import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.request.* import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -29,7 +30,7 @@ import kotlinx.coroutines.withContext internal class DefaultVisaActivationRepository @AssistedInject constructor( @Assisted private val visaCardId: VisaCardId, @NetworkMoshi private val moshi: Moshi, - private val visaApi: TangemPayApi, + private val visaApi: VisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, @@ -37,7 +38,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( private val apiConfigsManager: ApiConfigsManager, ) : VisaActivationRepository { - private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override suspend fun getActivationRemoteState(): Either = withContext(dispatcherProvider.io) { @@ -187,7 +188,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( responseError.errorBody != null ) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode return Either.Left(VisaApiError.fromBackendError(errorCode)) } @@ -207,7 +208,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( }.getOrElse { responseError -> if (responseError is ApiResponseError.HttpException && responseError.errorBody != null) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode Either.Left(VisaApiError.fromBackendError(errorCode)) } else { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt index ded2ed2d27..2f358e3a0f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt @@ -4,33 +4,35 @@ import arrow.core.Either import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.TangemPayAuthApi -import com.tangem.datasource.api.pay.models.request.* -import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.request.* import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSession +import com.tangem.domain.visa.model.VisaAuthSignedChallenge +import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject internal class DefaultVisaAuthRemoteDataSource @Inject constructor( @NetworkMoshi private val moshi: Moshi, - private val visaAuthApi: TangemPayApi, - private val tangemPayAuthApi: TangemPayAuthApi, + private val visaApi: VisaApi, private val dispatchers: CoroutineDispatcherProvider, ) : VisaAuthRemoteDataSource { - private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override suspend fun getCardAuthChallenge( cardId: String, cardPublicKey: String, ): Either = withContext(dispatchers.io) { request { - visaAuthApi.generateNonceByCardId( + visaApi.generateNonceByCardId( GenerateNoneByCardIdRequest( cardId = cardId, cardPublicKey = cardPublicKey, @@ -49,7 +51,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( cardWalletAddress: String, ): Either = withContext(dispatchers.io) { request { - visaAuthApi.generateNonceByCardWallet( + visaApi.generateNonceByCardWallet( GenerateNoneByCardWalletRequest( cardWalletAddress = cardWalletAddress, cardId = cardId, @@ -63,56 +65,13 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( } } - override suspend fun getCustomerWalletAuthChallenge( - customerWalletAddress: String, - customerWalletId: String, - ): Either = withContext(dispatchers.io) { - request { - tangemPayAuthApi.generateNonceByCustomerWallet( - request = GenerateNonceByCustomerWalletRequest( - customerWalletAddress = customerWalletAddress, - customerWalletId = customerWalletId, - ), - ).getOrThrow() - }.map { response -> - VisaAuthChallenge.Wallet( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), - ) - } - } - - override suspend fun getTokenWithCustomerWallet( - sessionId: String, - signature: String, - nonce: String, - ): Either = withContext(dispatchers.io) { - request { - tangemPayAuthApi.getTokenByCustomerWallet( - request = GetTokenByCustomerWalletRequest( - authType = "customer_wallet", - sessionId = sessionId, - signature = signature, - messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", - ), - ).getOrThrow() - }.map { response -> - TangemPayAuthTokens( - accessToken = response.accessToken, - expiresAt = response.expiresAt, - refreshToken = response.refreshToken, - refreshExpiresAt = response.refreshExpiresAt, - ) - } - } - override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { request { when (signedChallenge) { is VisaAuthSignedChallenge.ByCardPublicKey -> { - visaAuthApi.getAccessTokenByCardId( + visaApi.getAccessTokenByCardId( GetAccessTokenByCardIdRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, @@ -121,7 +80,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( ).getOrThrow() } is VisaAuthSignedChallenge.ByWallet -> { - visaAuthApi.getAccessTokenByCardWallet( + visaApi.getAccessTokenByCardWallet( GetAccessTokenByCardWalletRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, @@ -150,11 +109,11 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( request { when (refreshToken.authType) { VisaAuthTokens.RefreshToken.Type.CardId -> - visaAuthApi.refreshCardIdAccessToken( + visaApi.refreshCardIdAccessToken( RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), ) VisaAuthTokens.RefreshToken.Type.CardWallet -> - visaAuthApi.refreshCardIdAccessToken( + visaApi.refreshCardIdAccessToken( RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), ) }.getOrThrow() @@ -169,7 +128,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( override suspend fun exchangeAccessToken(tokens: VisaAuthTokens): Either = withContext(dispatchers.io) { request { - visaAuthApi.exchangeAccessToken( + visaApi.exchangeAccessToken( ExchangeAccessTokenRequest( accessToken = tokens.accessToken, refreshToken = tokens.refreshToken.value, @@ -194,7 +153,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( responseError.errorBody != null ) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode return Either.Left(VisaApiError.fromBackendError(errorCode)) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 2a81db61e6..2372e3639e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -14,8 +14,8 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet @@ -43,7 +43,7 @@ internal class DefaultVisaRepository @Inject constructor( private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, - private val visaApi: TangemPayApi, + private val visaApi: VisaApi, private val visaCurrencyFactory: VisaCurrencyFactory, ) : VisaRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt index 09128b4324..cc00430f80 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.converter -import com.tangem.datasource.api.pay.models.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse import com.tangem.domain.visa.model.VisaActivationOrderInfo import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.utils.converter.Converter diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a5efc3d025..0810a2640c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,10 +1,12 @@ package com.tangem.data.visa.di import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource +import com.tangem.data.visa.DefaultTangemPayRemoteDataSource import com.tangem.data.visa.DefaultVisaActivationRepository import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource import com.tangem.data.visa.MockVisaRepository import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.repository.VisaRepository @@ -22,22 +24,16 @@ internal interface VisaDataModule { @Singleton fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource + @Binds + @Singleton + fun bindTangemPayRemoteDataSource(impl: DefaultTangemPayRemoteDataSource): TangemPayRemoteDataSource + @Binds @Singleton fun bindVisaActivationRepositoryFactory( repository: DefaultVisaActivationRepository.Factory, ): VisaActivationRepository.Factory - // Mocked - // @Binds - // @Singleton - // fun bindVisaActivationRepositoryFactory( - // repository: MockVisaActivationRepository.Factory, - // ): VisaActivationRepository.Factory - - // @Binds - // fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository - // Mocked @Binds fun bindVisaRepository(repository: MockVisaRepository): VisaRepository diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index 04aa38e0d9..a5f2592c55 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -5,8 +5,8 @@ import com.tangem.data.visa.model.AccessCodeData import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet @@ -24,7 +24,7 @@ typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( private val userWalletsStore: UserWalletsStore, - private val visaAuthApi: TangemPayApi, + private val visaAuthApi: VisaApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, ) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt index b33a0cafb9..8f9b41148f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt @@ -2,7 +2,7 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxDetails internal class VisaTxDetailsFactory { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt index 6311b23e70..83cc10faa3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.utils -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.converter.Converter diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 01950d27b7..07ebc8a190 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -3,7 +3,7 @@ package com.tangem.data.visa.utils import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.data.common.cache.CacheRegistry -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt new file mode 100644 index 0000000000..ac5eeedaa1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.visa.datasource + +import arrow.core.Either +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayAuthTokens +import com.tangem.domain.visa.model.VisaAuthChallenge + +interface TangemPayRemoteDataSource { + + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + customerWalletId: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt index c5cbbee4b9..e559eb80aa 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt @@ -2,7 +2,6 @@ package com.tangem.domain.visa.datasource import arrow.core.Either import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSignedChallenge import com.tangem.domain.visa.model.VisaAuthTokens @@ -19,17 +18,6 @@ interface VisaAuthRemoteDataSource { cardWalletAddress: String, ): Either - suspend fun getCustomerWalletAuthChallenge( - customerWalletAddress: String, - customerWalletId: String, - ): Either - - suspend fun getTokenWithCustomerWallet( - sessionId: String, - signature: String, - nonce: String, - ): Either - suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either From ff024393f97c3d1d2e3b265ff7d5ceb2cbc97bb6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 15:20:04 +0500 Subject: [PATCH 080/130] Updated on 2026-08-14 --- app/build.gradle.kts | 5 ++--- app/libs/dexprotector-annotations.jar | Bin 0 -> 3531 bytes .../DefaultDeviceSecurityInfoProvider.kt | 13 ++++++++++++ .../tap/di/core/security/SecurityModule.kt | 20 ++++++++++++++++++ core/security/.gitignore | 1 + core/security/build.gradle.kts | 4 ++++ .../security/DeviceSecurityInfoProvider.kt | 7 ++++++ settings.gradle.kts | 15 +++++++------ 8 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 app/libs/dexprotector-annotations.jar create mode 100644 app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt create mode 100644 core/security/.gitignore create mode 100644 core/security/build.gradle.kts create mode 100644 core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bb2b8ab488..d4fb854892 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -179,6 +179,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.decompose) implementation(projects.core.error.ext) + implementation(projects.core.security) implementation(projects.libs.crypto) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) @@ -353,7 +354,6 @@ dependencies { /** DI */ implementation(deps.hilt.android) - kapt(deps.hilt.kapt) kapt(deps.hilt.compilerx) @@ -385,10 +385,10 @@ dependencies { implementation(deps.prettyLogger) implementation(deps.decompose.ext.compose) implementation(deps.moshi.adapters) - implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) + implementation(files("libs/dexprotector-annotations.jar")) /** Testing libraries */ testImplementation(projects.common.test) @@ -418,7 +418,6 @@ dependencies { implementation(deps.camera.camera2) implementation(deps.camera.lifecycle) implementation(deps.camera.view) - implementation(deps.listenableFuture) implementation(deps.mlKit.barcodeScanning) diff --git a/app/libs/dexprotector-annotations.jar b/app/libs/dexprotector-annotations.jar new file mode 100644 index 0000000000000000000000000000000000000000..3f2f98f07fec15899e07325897e3ff62efca8203 GIT binary patch literal 3531 zcmai$2|U#4AIIIf*_lBoIfpRLCUS(4b6kz1WCzo@b2T%_l^AEPwF)UA$3Kc#u_i}I z*%b5S5GF(@r4jho}>Bm1Mn!s@as>@O496D|=h5omVWqxsq%rK9i0 zK$)R^yuLrcKRtf==HcXg$rs~;b4KHQFh6lWyq`PD+uH|+!nyl+V}BCLvtK9%hyEV2 zt*#Q+3H&>Xt1m4L4P?KN-PH`;ot$;hXlE=|4(*A;VxuewX7q~Op{b?{kzy=|d8}v! zs&ewLGBAtFK@3v5mpEHf(0P?A5e|8i^y^a1kr4&jcMf(5)}i%3EN!fEPIBL5)r_id zx{@wQ5cvi!4U~KQpR{;$}t$02-SHLBLzk2j?xGei<0sc+G+u zjI_01?!fW@Zi0#(h#4v|fKR5YADyJnHLYgPXU@kkz~FB`vw6V;58p_0YA0I2AZ(r} zM&#T)ti-saydiOqa_#qxBv&blg;11J{x9V$aTs@RS3PeuCeWAq=63W`htunDlfmiL z0uFJGr~>O=*n_f%fCS<7mv?yQj;AYn4lP92)j5(gqqG{1NOap?5{-DOF!5f~C1703 zeuC!Kc>|PPZeFj1lz)&@pot-8a&+V(0ekYTTW!sxD{>?wHinrLF0b32PEZ<#> zibH4Jc|grFvLzAv+#wfFUd7$)kP%Bo7miKn7qhhB@Nb+#jTJoR@ zP0J8s$ZA#!$8t4G$W{pW`xm@O?r1=_!QsRJ*UfymAaIF0H}p-1=o>8hMp>wbA2WSA zU%ah^X67sbyx>$2@6w(OKFCs%M~8a%2UWy3R$)MQVhS?KjJbqRRgk|%P8konupD*J zYSlM1>>^b7(9Gq`qzz}A_13_Zr(9^mu)4I~BEA<1;Pb)@WI_cQmaOe&7&?|`+)>Lr%Hi-_&)E`c%9&^S>}SMmKsq;7l!4sA&}H~hYB z-lsNRSfDlN^6}xBL=syih%J(iwU79o| z4N-%PWLt;iCf5;l_C@`2amP0}a2Uk0-MZttiF4tv5Hl8T5bc=lC`NN&*~Pkl@JT1K z!Lj9~wv&xc1DW$e?%5(~#)3Oe%k3XuFxc?`%4*Qos~9=;0+5^{_H09MnrU&M#7&uN zfuPe3+AR^k7Fo$g4bF)PF9<+szif=YJq-0Kw$Vhc9(q!o&BGnwHwg8F=Q3U#C_Yhj z8jUS443g%^MD{Z_dwi;OcreeI;+u7= z__ab6bLG_@6$Yu1)U1{vJ#MoV(HA;z1`+@8H2)}73m31-|8q|FC^6HHx8tELR;@rj zRrj&ajjF1gL88~FvR5VXuXtI=h#Z@QT7H3Av22dVA5QI{4#}3*yD0wgj4M_iG7@&s z_EuT1^w+0WW;o#T>xqG5MR!*i60GekyKDy(GY(1Vy-U*x=X?dNNhzI!$0w2I1b#{L z*MK0GAG%1d_|ie~PVsAuODoiV}26*u$OU14`&D51UmLXy+O#}r8I_vWdjyIixgjWAGm6_aMvV~9j+P&1dO}-CN& zh9`PT`2E(d;Zs+(taL`5T=FFIq~bf&h{8|{-=wGb49Pl}sSPx21rQM=V| zg?wIzENU~dNA-}^i^88BPc*2UE3gwBiV|69Q9#BX7|M#qiy!ewnGoLS;@wbaGdTC^ zPeZfHRx$Ao$nxn&Sw#@H`cyxrBmE%>(^=YuC#S^x7><}TO&2-lFXm>MeF)y7|D@-( z#zlB-QufkQA&F_4;CP6Q_(1~}>7V;4bxpOvUjP-GA1a|0BfOQ+E6}uD4vocNIFJ}b zh!pwC-F0B|djU~#Ew}ztubs}+fWeYtq|EAD2~`&W7^UZ>!y=NO)n14r{MP+ai=UJm zES}Syuv{(HN%KvRW)0H4`o4_*d!#9E<`y6%UQ4QbJr~igcS}1h}3FR2zEe%`MP$O!8O~-PJ~+n7&Q( z{x5(!>=*UVpP%$BjR47#&e$5C*4M11OmYT<>zB>d?_BQ4Gd2}7Zq$*<1E_LBw=|)L z?bfz96+1m;?L;8(x3g^QCT%@s^K^}q>ud{)^L9c)4fFSsHvPDF-pdxM@f_)ax$1-3i+taH-Bz%MX{NjNij{;qLCjR7C#j{%xQ#=(bS{ z6DiYeBHgLEKa>85nEe*9hfx1LoXV)%MlHxk`5UJe{9E{LhWuIYRA&8=3b9|RZGzqD zW`Bm=Ns=EPwO@+e-RvdWAKh)I-V1p@Qa`5qj|bf!`Supri$6aKFj3y*PnFykhW6&) hO9nsk!zsM*kNo>^f(4wBi5kU9`OHury65YH|35;FYkU9z literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt new file mode 100644 index 0000000000..7463e33236 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.core.security + +import com.dexprotector.rtc.RtcStatus +import com.tangem.security.DeviceSecurityInfoProvider + +internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { + override val isRooted: Boolean + get() = RtcStatus.getRtcStatus().root + override val isBootloaderUnlocked: Boolean + get() = RtcStatus.getRtcStatus().unlockedBootloader + override val isXposed: Boolean + get() = RtcStatus.getRtcStatus().xposed +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt b/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt new file mode 100644 index 0000000000..b4e64ccb37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.core.security + +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.tap.core.security.DefaultDeviceSecurityInfoProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SecurityModule { + + @Provides + @Singleton + fun provideDeviceSecurityInfoProvider(): DeviceSecurityInfoProvider { + return DefaultDeviceSecurityInfoProvider() + } +} \ No newline at end of file diff --git a/core/security/.gitignore b/core/security/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/security/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/security/build.gradle.kts b/core/security/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/core/security/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt new file mode 100644 index 0000000000..5be549d653 --- /dev/null +++ b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt @@ -0,0 +1,7 @@ +package com.tangem.security + +interface DeviceSecurityInfoProvider { + val isRooted: Boolean + val isBootloaderUnlocked: Boolean + val isXposed: Boolean +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 6a05fead77..50802ec815 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -153,19 +153,20 @@ include(":test:core") include(":test:mock") // region Core modules +include(":core:ab-tests") include(":core:analytics") include(":core:analytics:models") -include(":core:datasource") include(":core:config-toggles") -include(":core:ab-tests") -include(":core:navigation") -include(":core:res") -include(":core:ui") -include(":core:utils") +include(":core:datasource") include(":core:decompose") -include(":core:pagination") include(":core:error") include(":core:error:ext") +include(":core:navigation") +include(":core:pagination") +include(":core:res") +include(":core:security") +include(":core:ui") +include(":core:utils") // endregion Core modules // region Common modules From 2e10db5bd558df3bba9935d0b2075e5b1387fee3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 10:52:06 +0000 Subject: [PATCH 081/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d167ffb2eb..e5b0dbadd1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "develop-1327" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +tangemCardSdk = "develop-573" #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 32c24cc0bbebab6ddb7fe01727346885e9f99841 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 14:54:30 +0400 Subject: [PATCH 082/130] Updated on 2026-08-14 --- .../pay/models/response/CheckCustomerWalletResponse.kt | 9 +++++++-- .../data/pay/repository/DefaultOnboardingRepository.kt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt index 11f891c668..b926f0fa05 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -5,5 +5,10 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CheckCustomerWalletResponse( - @Json(name = "id") val id: String?, -) \ No newline at end of file + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + data class Result( + @Json(name = "id") val id: String?, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 2e66473cae..c8eb5b8e48 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -181,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor( customerWalletId = userWalletId.stringValue, ) }.map { response -> - val id = response.id + val id = response.result?.id val isPaeraCustomer = !id.isNullOrEmpty() tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, isPaeraCustomer) isPaeraCustomer From 8b40cf488909bc9aabc7a4f04292d377e6da4293 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 14:52:24 +0300 Subject: [PATCH 083/130] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../wallet/child/wallet/model/WalletModel.kt | 13 ++---- .../model/WalletsUpdateActionResolver.kt | 4 +- .../ReinitializeNewWalletTransformer.kt | 46 +++++++++++++++++++ .../ReinitializeWalletTransformer.kt | 30 ++++-------- 5 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 5fc86d29bc..7697824250 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 5fc86d29bc2c0dc7c057ab0242b2cfa3e9f48daf +Subproject commit 76978242504ab90803b99e2b0575f7ebe8e69335 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 4b707d963b..b136451f60 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 @@ -453,7 +453,7 @@ internal class WalletModel @Inject constructor( private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) - is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolver.Action.ReinitializeNewWallet -> reinitializeNewWallet(action) is WalletsUpdateActionResolver.Action.ReinitializeWallets -> reinitializeWallets(action) is WalletsUpdateActionResolver.Action.AddWallet -> addWallet(action) is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action) @@ -537,7 +537,7 @@ internal class WalletModel @Inject constructor( } } - private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { + private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) { walletScreenContentLoader.cancel(action.prevWalletId) tokenListStore.remove(action.prevWalletId) @@ -550,7 +550,7 @@ internal class WalletModel @Inject constructor( fetchWalletContent(userWallet = action.selectedWallet) stateHolder.update( - ReinitializeWalletTransformer( + ReinitializeNewWalletTransformer( prevWalletId = action.prevWalletId, newUserWallet = action.selectedWallet, clickIntents = clickIntents, @@ -570,14 +570,11 @@ internal class WalletModel @Inject constructor( coroutineScope = modelScope, ) - modelScope.launch(dispatchers.main) { - fetchWalletContent(userWallet = userWallet) - } + fetchWalletContent(userWallet = userWallet) stateHolder.update( ReinitializeWalletTransformer( - prevWalletId = userWallet.walletId, - newUserWallet = userWallet, + userWallet = userWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, ), 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 82846a7624..d498e480a0 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 @@ -74,7 +74,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( getChangeWalletsListAction(state, wallets, selectedWallet) } isAnotherWalletSelected(state, selectedWallet) -> { - Action.ReinitializeWallet( + Action.ReinitializeNewWallet( prevWalletId = state.getPrevSelectedWallet().id, selectedWallet = selectedWallet, ) @@ -291,7 +291,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( * @property prevWalletId previous selected wallet id * @property selectedWallet selected wallet */ - data class ReinitializeWallet( + data class ReinitializeNewWallet( val prevWalletId: UserWalletId, val selectedWallet: UserWallet, ) : Action() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt new file mode 100644 index 0000000000..d6072f30f7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory +import kotlinx.collections.immutable.toImmutableList + +/** + * Reinitialize and place new wallet at the end transformer + * + * @property prevWalletId reinitialized wallet id + * @property newUserWallet new user wallet + * @property clickIntents click intents + * +[REDACTED_AUTHOR] + */ +internal class ReinitializeNewWalletTransformer( + private val prevWalletId: UserWalletId, + private val newUserWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val walletImageResolver: WalletImageResolver, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { + WalletLoadingStateFactory( + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + ) + } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = prevState.wallets + .filterNot { it.walletCardState.id == prevWalletId } + .plus( + element = walletLoadingStateFactory.create( + userWallet = newUserWallet, + ), + ) + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 4f4fa993ca..4459476877 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -1,28 +1,23 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import kotlinx.collections.immutable.toImmutableList /** * Reinitialize wallet transformer * - * @property prevWalletId reinitialized wallet id - * @property newUserWallet new user wallet - * @property clickIntents click intents - * -[REDACTED_AUTHOR] + * @property userWallet user wallet to reinitialize + * @property clickIntents click intents + * @property walletImageResolver wallet image resolver */ internal class ReinitializeWalletTransformer( - private val prevWalletId: UserWalletId, - private val newUserWallet: UserWallet, + private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, -) : WalletScreenStateTransformer { +) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( @@ -31,16 +26,9 @@ internal class ReinitializeWalletTransformer( ) } - override fun transform(prevState: WalletScreenState): WalletScreenState { - return prevState.copy( - wallets = prevState.wallets - .filterNot { it.walletCardState.id == prevWalletId } - .plus( - element = walletLoadingStateFactory.create( - userWallet = newUserWallet, - ), - ) - .toImmutableList(), + override fun transform(prevState: WalletState): WalletState { + return walletLoadingStateFactory.create( + userWallet = userWallet, ) } } \ No newline at end of file From 9c6ce006aa283be31add975803eada3645c36b25 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 16:55:33 +0500 Subject: [PATCH 084/130] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 28 +++++++------ .../security/DeviceSecurityInfoProvider.kt | 4 +- data/visa/build.gradle.kts | 1 + .../tangem/data/pay/di/TangemPayDataModule.kt | 3 ++ .../repository/DefaultOnboardingRepository.kt | 2 +- domain/visa/build.gradle.kts | 5 ++- .../pay/model/TangemPayCustomerInfoError.kt | 1 + .../TangemPayMainScreenCustomerInfoUseCase.kt | 13 ++++++ .../model/TangemPayOnboardingModel.kt | 2 +- .../wallet/state/model/TangemPayState.kt | 2 + .../TangemPayExposedDeviceTransformer.kt | 17 ++++++++ .../subscribers/TangemPayMainSubscriber.kt | 5 ++- .../visa/TangemPayExposedDeviceState.kt | 41 +++++++++++++++++++ .../visa/TangemPayMainScreenBlock.kt | 5 ++- 14 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d365c6e247..524e2ec97f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -103,10 +103,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. - You’ll be asked for your wallet’s access code later so we can securely store it for future use - This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + You\'ll be asked for your access code later for secure storage. + This will delete all saved wallet access codes. You\'ll need to enter the access code again to use the wallet. Removing the saved devices deletes all the saved wallets and their access codes from the app. - This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. + This will delete all saved wallet access codes. You’ll need to enter the access code again to use the wallet. Require Access Code This option turns off biometrics for sensitive actions. You’ll need to enter your access code each time you sign a transaction. Save Access Code @@ -145,10 +145,10 @@ Balances are hidden According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates! Beta Mode - Biometrics are turned off on your device, so you can’t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. + Biometrics are turned off on your device, so you can\'t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. Biometric authentication disabled Please scan the card or ring - You’ve reached the limit of biometric attempts. Please unlock your wallet with a device tap or enter your access code. + You\'ve reached the limit of biometric attempts. Please unlock your wallet with a card/ring or enter your access code. Biometric authentication locked Please try again in 30 seconds or scan the card or ring Biometric login is temporarily locked. Please try again in 30 seconds, or unlock your wallet with a device tap or access code. @@ -182,7 +182,7 @@ Upgrade again Reset complete We recommend completing the reset process for all Tangem devices in this wallet. - You haven’t reset all your Tangem devices + Some Tangem devices still need to be reset. Disable this option if you don\'t want this card to be used to reset access codes on other cards or rings in this wallet. Please note that this will also prevent you from resetting the access code on this card. Allows you to use this card to reset access code on other cards in this wallet Access code recovery @@ -384,8 +384,9 @@ Transaction status Transactions Transfer - Unable to load the data… + Unable to load data… I understand + I understand, continue There was an error. Please try again. Unreachable Unstake @@ -575,7 +576,7 @@ Create New Wallet Order Tangem Scan Tangem - Do you want to allow “Tangem” to use biometric authentication? To confirm your identity and open the app + Do you want to allow \"Tangem\" to use biometric authentication to confirm your identity and open the app? to %s On %s network Are you sure you want to cancel access code setup? @@ -799,7 +800,7 @@ 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 + You must update to %1$s before creating a mobile wallet Mobile Wallet requires %1$s or later All news @@ -1426,6 +1427,7 @@ Your card is frozen. Get Help Other + Unable to use on rooted devices Completed Declined Pending @@ -1598,7 +1600,7 @@ Would you like to use\nPush-notifications? Enable push notifications to receive alerts when funds arrive in your wallet. Don\'t Miss a Transaction - Add new wallet + Add Wallet If you delete this wallet without a backup, you will permanently lose access to your funds. Are you sure you want to forget this wallet? An error has occurred, please scan your card or ring to log in @@ -1759,7 +1761,7 @@ Change access code Stay notified on wallet incoming transactions and Tangem updates. Push notifications may currently not work on Huawei devices. We\'re actively working on a solution and will release a fix in an upcoming update. Thank you for your understanding! - Transaction Notifications + Transaction notifications Set access code Wallet settings Tangem @@ -1952,7 +1954,7 @@ Suspicious transaction Already have Tangem Wallet? Thousands of assets - Best in class hardware wallet + Top-tier hardware wallet Fast delivery Start in one tap Seamless and secure @@ -1961,7 +1963,7 @@ Create or import a software wallet Create or import a software wallet on your phone. Start with Mobile Wallet - Other method + Other methods Use a Tangem hardware wallet Learn more & buy Discard diff --git a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt index 5be549d653..09d16c0a94 100644 --- a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt +++ b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt @@ -4,4 +4,6 @@ interface DeviceSecurityInfoProvider { val isRooted: Boolean val isBootloaderUnlocked: Boolean val isXposed: Boolean -} \ No newline at end of file +} + +fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 287bd65478..1470c2b5d9 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /** Project - Data */ implementation(projects.core.datasource) implementation(projects.core.error) + implementation(projects.core.security) implementation(projects.data.common) /** Project - Domain */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index ff79c711bf..ffcd79fb85 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository +import com.tangem.security.DeviceSecurityInfoProvider import dagger.Binds import dagger.Module import dagger.Provides @@ -76,12 +77,14 @@ internal interface TangemPayDataModule { customerOrderRepository: CustomerOrderRepository, tangemPayOnboardingRepository: OnboardingRepository, eligibilityManager: TangemPayEligibilityManager, + deviceSecurity: DeviceSecurityInfoProvider, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( repository = repository, customerOrderRepository = customerOrderRepository, tangemPayOnboardingRepository = tangemPayOnboardingRepository, eligibilityManager = eligibilityManager, + deviceSecurity = deviceSecurity, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index c8eb5b8e48..d2059f3785 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -49,7 +49,7 @@ internal class DefaultOnboardingRepository @Inject constructor( return requestHelper.performWithStaticToken { tangemPayApi.validateDeeplink(body = DeeplinkValidityRequest(link = link)) }.map { response -> - response.result?.status == VALID_STATUS + response.result?.status.equals(VALID_STATUS, ignoreCase = true) } } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 1f83cc44b1..3c13ebe644 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -12,9 +12,10 @@ android { dependencies { /** Project - Core */ api(projects.core.pagination) - implementation(projects.core.utils) - implementation(projects.core.error) implementation(projects.core.analytics.models) + implementation(projects.core.error) + implementation(projects.core.security) + implementation(projects.core.utils) /** Project - Domain */ api(projects.domain.models) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt index 7fda6cfe51..e2086cfed3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt @@ -5,4 +5,5 @@ sealed interface TangemPayCustomerInfoError { data object UnavailableError : TangemPayCustomerInfoError data object RefreshNeededError : TangemPayCustomerInfoError data object UnknownError : TangemPayCustomerInfoError + data object ExposedDeviceError : TangemPayCustomerInfoError } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index b84b7f061b..10a21fbb78 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -9,6 +9,8 @@ import com.tangem.domain.pay.model.* import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed import kotlinx.coroutines.flow.* import timber.log.Timber @@ -23,6 +25,7 @@ class TangemPayMainScreenCustomerInfoUseCase( private val customerOrderRepository: CustomerOrderRepository, private val tangemPayOnboardingRepository: OnboardingRepository, private val eligibilityManager: TangemPayEligibilityManager, + private val deviceSecurity: DeviceSecurityInfoProvider, ) { val state: StateFlow>> @@ -30,6 +33,16 @@ class TangemPayMainScreenCustomerInfoUseCase( suspend fun fetch(userWalletId: UserWalletId) { Timber.tag(TAG).i("fetch: $userWalletId") + + if (deviceSecurity.isSecurityExposed()) { + Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + + updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) + return // fast exit + } + repository.checkCustomerWallet(userWalletId) .fold( ifLeft = { error -> 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 ff881de155..bc39e6d47b 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 @@ -104,7 +104,7 @@ internal class TangemPayOnboardingModel @Inject constructor( else -> back() } } - .onLeft { back() } + .onLeft { startOnboarding(userWalletId) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt index 25c8c2f0a5..4eae554a24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -44,4 +44,6 @@ internal sealed class TangemPayState { ) : TangemPayState() data class TemporaryUnavailable(val notification: WalletNotification) : TangemPayState() + + data object ExposedDevice : TangemPayState() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt new file mode 100644 index 0000000000..b8a735a4f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayExposedDeviceTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy(tangemPayState = TangemPayState.ExposedDevice) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 2f3bad4547..6a7cf0a950 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -58,7 +58,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( transformer = TangemPayUnavailableStateTransformer(userWalletId), ) } - else -> { + TangemPayCustomerInfoError.ExposedDeviceError -> { + stateController.update(TangemPayExposedDeviceTransformer(userWalletId)) + } + TangemPayCustomerInfoError.UnknownError -> { // hide TangemPay block Timber.e("Failed when loading main screen TangemPay info: $tangemPayError") stateController.update( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt new file mode 100644 index 0000000000..6ba66da79f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import com.tangem.core.ui.R +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayExposedDeviceState(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + onClick = {}, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index 7ee74d5ec2..b8f7e9a587 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -24,7 +24,8 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier) is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier) is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier) - TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier) + is TangemPayState.ExposedDevice -> TangemPayExposedDeviceState(modifier) + is TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier) } } @@ -36,6 +37,8 @@ private fun TangemPayMainScreenBlockPreview() { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false) + TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) + TangemPayMainScreenBlock( Progress( title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), From 6c055d4b525461394ab10a002a6c009058e80614 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 15:38:42 +0300 Subject: [PATCH 085/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e5b0dbadd1..e9ce1c6a8f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-573" #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-532" +tangemHotSdk = "develop-536" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From c8ff2f83fdafee6fd118f06b560475dfc2e33185 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 16:05:03 +0300 Subject: [PATCH 086/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e9ce1c6a8f..e51eb53245 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-573" #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-536" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 5a8678ea62cae3e33ca382859949249724599824 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 18:05:51 +0500 Subject: [PATCH 087/130] Updated on 2026-08-14 --- .../utils/WalletWarningsSingleEventSender.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) 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 a51f2133e4..69b492660a 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,6 +1,5 @@ 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 @@ -22,6 +21,7 @@ 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 +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @ModelScoped @@ -32,8 +32,7 @@ internal class WalletWarningsSingleEventSender @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val router: Router, ) { - - private val isActivationBottomSheetShown: AtomicBoolean = AtomicBoolean(false) + private val isActivationBottomSheetShown: ConcurrentHashMap = ConcurrentHashMap() suspend fun send( userWalletId: UserWalletId, @@ -46,16 +45,26 @@ internal class WalletWarningsSingleEventSender @Inject constructor( val events = newWarnings.filter { it !in displayedUiState.warnings } + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + events.forEach { event -> when (event) { is WalletNotification.Critical.SeedPhraseNotification -> { seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) } is WalletNotification.FinishWalletActivation -> { - if (event.type == WalletActivationBannerType.Warning && !isActivationBottomSheetShown.get()) { - showFinishActivationBottomSheet(userWalletId) + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.type == WalletActivationBannerType.Warning) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = true } - isActivationBottomSheetShown.set(true) } else -> Unit } From 46c112eca80aefe6b9d57e6dfe21b2e1bbb3e285 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 16:24:18 +0300 Subject: [PATCH 088/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e51eb53245..462e99b59c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1327" +tangemBlockchainSdk = "releases-5.32-1329" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-573" +tangemCardSdk = "releases-5.32-574" #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 ^ From ed89b4bcd0305ffc91899389d9234f0d3a86371f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:10:58 +0500 Subject: [PATCH 089/130] Updated on 2026-08-14 --- .../CardContextInterceptor.kt | 1 + .../HotWalletContextInterceptor.kt | 1 + .../ui/userwallet/UserWalletUnlockError.kt | 11 ++++++-- .../core/analytics/models/event/SignIn.kt | 4 ++- .../details/model/UserWalletListModel.kt | 3 ++- .../stepper/impl/HotWalletStepperModel.kt | 1 - .../intents/WalletWarningsClickIntents.kt | 1 + .../welcome/impl/model/WelcomeModel.kt | 26 ++++++++++++++----- 8 files changed, 37 insertions(+), 11 deletions(-) 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 cdd42cdde9..5b5902f3cc 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 @@ -36,6 +36,7 @@ class CardContextInterceptor( is SignIn.ScreenOpened, is SignIn.ButtonAddWallet, -> false + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll 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 f67eaab3f8..e24d5594e5 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 @@ -19,6 +19,7 @@ class HotWalletContextInterceptor( is SignIn.ButtonUnlockAllWithBiometric, is IntroductionProcess.ButtonScanCard, -> false + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll else -> true } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index e7e4b2a884..41fdd9e856 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -16,6 +16,7 @@ inline fun UnlockWalletError.handle( onAlreadyUnlocked: () -> Unit = {}, onUserCancelled: () -> Unit = {}, analyticsEventHandler: AnalyticsEventHandler, + isFromUnlockAll: Boolean, noinline showMessage: (EventMessage) -> Unit, ) { when (this) { @@ -33,11 +34,17 @@ inline fun UnlockWalletError.handle( // This should never happen in this flow, as we always check for the wallet existence before unlocking showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error))) } - is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, analyticsEventHandler, showMessage) + is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock( + isFromUnlockAll = isFromUnlockAll, + error = this, + analyticsEventHandler = analyticsEventHandler, + showDialog = showMessage, + ) } } fun handleUnableToUnlock( + isFromUnlockAll: Boolean, error: UnlockWalletError.UnableToUnlock, analyticsEventHandler: AnalyticsEventHandler, showDialog: (DialogMessage) -> Unit, @@ -46,7 +53,7 @@ fun handleUnableToUnlock( is UnlockWalletError.UnableToUnlock.WithReason -> { when (error.reason) { Reason.AllKeysInvalidated -> { - analyticsEventHandler.send(SignIn.ErrorBiometricUpdated()) + analyticsEventHandler.send(SignIn.ErrorBiometricUpdated(isFromUnlockAll)) DialogMessage( title = resourceReference(R.string.biometric_updated_warning_title), message = resourceReference(R.string.biometric_updated_warning_description), 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 index 9b88374046..f62e70074f 100644 --- 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 @@ -21,7 +21,9 @@ sealed class SignIn( class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric") - class ErrorBiometricUpdated : SignIn(event = "Error - Biometric Updated") + data class ErrorBiometricUpdated( + val isFromUnlockAll: Boolean, + ) : SignIn(event = "Error - Biometric Updated") class ButtonWallet( signInType: SignInType, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index f0b9432ebf..badebd605e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -91,7 +91,7 @@ internal class UserWalletListModel @Inject constructor( private fun onAddNewWalletClick() { if (hotWalletFeatureToggles.isHotWalletEnabled) { - analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.Settings)) router.push(AppRoute.CreateWalletSelection) } else { withProgress(isWalletSavingInProgress) { @@ -109,6 +109,7 @@ internal class UserWalletListModel @Inject constructor( Timber.e("Failed to unlock wallet $userWalletId: $error") error.handle( onUserCancelled = {}, + isFromUnlockAll = false, onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, analyticsEventHandler = analyticsEventHandler, showMessage = messageSender::send, 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 5e26e5570d..3f86e09a9d 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 @@ -31,7 +31,6 @@ internal class HotWalletStepperModel @Inject constructor( } fun onSkipClick() { - // TODO send analytics params.callback.onSkipClick() } } \ No newline at end of file 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 aba0e7c275..8d1c6bd283 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 @@ -200,6 +200,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( onAlreadyUnlocked = {}, onUserCancelled = {}, analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = true, showMessage = uiMessageSender::send, ) } 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 249109f0e4..b5b89566f2 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 @@ -108,9 +108,10 @@ internal class WelcomeModel @Inject constructor( routedOut = true router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle( + .onLeft { error -> + error.handle( specificWalletId = null, + isFromUnlockAll = true, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }, ) setSelectWalletState() @@ -146,8 +147,12 @@ internal class WelcomeModel @Inject constructor( .onRight { router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle(null, onUserCancelled = { /* ignore */ }) + .onLeft { error -> + error.handle( + specificWalletId = null, + isFromUnlockAll = true, + onUserCancelled = { /* ignore */ }, + ) } } }, @@ -202,11 +207,19 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) } .onLeft { error -> - error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + error.handle( + specificWalletId = userWalletId, + isFromUnlockAll = false, + onUserCancelled = { /* ignore*/ }, + ) } } - suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { + suspend fun UnlockWalletError.handle( + specificWalletId: UserWalletId?, + isFromUnlockAll: Boolean, + onUserCancelled: suspend () -> Unit = { }, + ) { handle( onAlreadyUnlocked = { // this should not happen, as we check for locked state before this @@ -215,6 +228,7 @@ internal class WelcomeModel @Inject constructor( }, onUserCancelled = { onUserCancelled() }, analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = isFromUnlockAll, showMessage = uiMessageSender::send, ) } From 533c0368190b9db88aea0f55d6fc3e13a9f5ebde Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:20:51 +0500 Subject: [PATCH 090/130] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 9 +- .../response/TokenMarketListResponse.kt | 1 + .../local/preferences/PreferencesKeys.kt | 4 +- ...g_yield_supply_in_market_notification.webp | Bin 0 -> 18094 bytes .../markets/DefaultMarketsTokenRepository.kt | 27 +++- .../converters/TokenListConfigConverters.kt | 1 + .../converters/TokenMarketListConverter.kt | 2 +- .../data/markets/di/MarketsDataModule.kt | 1 - .../data/promo/DefaultPromoRepository.kt | 8 +- .../com/tangem/domain/markets/TokenMarket.kt | 2 +- .../domain/markets/TokenMarketListConfig.kt | 2 +- .../GetStakingNotificationMaxApyUseCase.kt | 39 ----- .../ShouldShowYieldModeMarketPromoUseCase.kt | 24 +++ .../repositories/MarketsTokenRepository.kt | 5 +- .../tangem/domain/promo/PromoRepository.kt | 4 +- .../converter/MarketsTokenItemConverter.kt | 2 +- .../feed/model/feed/FeedComponentModel.kt | 1 + .../feed/state/FeedMarketsBatchFlowManager.kt | 1 + .../feed/ui/market/state/MarketsListUM.kt | 1 + .../analytics/MarketsListAnalyticsEvent.kt | 8 +- .../tokenlist/impl/model/MarketsListModel.kt | 81 ++++++---- .../impl/model/MarketsNotificationUM.kt | 22 +++ .../converters/MarketsTokenItemConverter.kt | 2 +- .../MarketsListBatchFlowManager.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 29 ++-- .../markets/tokenlist/impl/ui/MarketsList.kt | 65 ++++---- .../YieldSupplyInMarketsPromoNotification.kt | 147 ++++++++++++++++++ .../tokenlist/impl/ui/state/MarketsListUM.kt | 7 +- 28 files changed, 334 insertions(+), 162 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp delete mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt 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 d6bfbdb63a..6e154f3fc1 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 @@ -8,7 +8,6 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -129,13 +128,11 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetStakingNotificationMaxApyUseCase( - settingsRepository: SettingsRepository, + fun provideShouldShowYieldModeMarketPromoUseCase( promoRepository: PromoRepository, marketsTokenRepository: MarketsTokenRepository, - ): GetStakingNotificationMaxApyUseCase { - return GetStakingNotificationMaxApyUseCase( - settingsRepository = settingsRepository, + ): ShouldShowYieldModeMarketPromoUseCase { + return ShouldShowYieldModeMarketPromoUseCase( promoRepository = promoRepository, marketsTokenRepository = marketsTokenRepository, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 3c1829900b..31168b8c5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -26,6 +26,7 @@ data class TokenMarketListResponse( @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @Json(name = "staking_opportunities") val stakingOpportunities: List?, + @Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?, ) { @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 3fafa4462a..b52614222b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -77,8 +77,8 @@ object PreferencesKeys { val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } - val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { - booleanPreferencesKey(name = "marketsStakingNotificationHideClicked") + val MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy { + booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked") } val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") } diff --git a/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp b/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp new file mode 100644 index 0000000000000000000000000000000000000000..6f7bfc38ce451bddf180a6c653af3ed8ddf30629 GIT binary patch literal 18094 zcmaf(Q;;Q06QP$Xrq)}E*b6Om*g+ek@<*3 z(ds{z_(=Ly{D#h)v@(>_A*~~`9W|tA21;X$IWCJq)ZvlFF zLQa;jAs}vE|D#pU#bVC>`LcQ=9LU$wkRqMmyZfojFIRCa=={BCfu@GP*CP{amC}Wc zyljE8$gh-@m!+V4>UTvp?|cquwJCzQGa)19V;Toj;~6QI4SWM-mOcT3arV z7amumXptGgw5jtig90!~kpbPxA2&16nBoN3#nQaoHS=+r?p+>rKKWtuC5Z@&1<*AZ zF|gnh@``#rs?yWT1DI(FA@JMa;xe|^=hQE%)oe4lBh!SRt0afbt|E@h(P_Dl?fF-Mk#A3SBuFORz41Pd#>3;krc)SrIs7+c(wOE^uXfld{fX zCgFvBz~?=h5xPQbi8`7c=}qjOyXm_cEoHT8{jh&y2d%^ga6L|nF&P}eP`~{TSs7=F zPfnCiWZyA_RSHKvE4@G0~SVLQn0F$F@wfVtaWb* zz4aDcvA~GN&Q3l~JixP`vpnw^lbb~2opy_M^$}mg6_dR!7%I>l5zy?ECb7v8_%Tj7 zb{ljb^IPb9NSKd+@(9C!b7ZwmF5GIbSBf%3LkVoT8yk@2g3KLH(eVAt*!^lwRva=% zg#wY!yT&ChB@FC6eHKIb@v5{4Ol;gvAkziHYC;+>@W#%6&h?^!^;xv1U&7XgH{*WC@0-MgYgj^85roS^9gufg@{lSt<`mWACq~U zk5Iy2g6`wy4SIBn%iYYuR`UDoY@-(oBicCvnf+6Fa1?QCyXkySIe4TuOME#(s+ka})6QJ-1#|$&6J#U&o%fw!S3l!d!CTj~~lqzPlolc!`{RZYLd~^IlyT&Kyn`%p%Ok zU`BB^?ZIhwcom42ttR?WR3DT0U+S=>fgULp8WF~8s=pGQF^d43RQ#{C>oOSDM-Efw ziDkG9>FV+n6D&)tlmb?p1RdF2F1X*f=+F}sRR)90GBEIQFhO1QWTOf-c{Bzk#7k{V zsZSzZ%RUR^r`6CYV&57%4SEW+NdNXmCwgmUF~6H2@}gS0_x8$#NeeD_zZkJbNCQ+s z72MJouFKud0hBc9__sz;Fija4?O(E;V;F0ykR%QSl-R0Bk~`&Wj7v_&*b>og7$)Pm z?2_xx&p7732yyO#&Bl-*@u=Ew8W6hS70IW|?d|PtGeKYN!sVz*ZBm6b=+uhFt*w3K zx0TdMn;>1)tGbUg7NBn8db-+Vp6VzNa2sXmlP%eu{OAz8{YWkemCew&-1m04yW2zl->!3|y~XQEJo?i_c!KopUdmBYz8Q+n?Bft2%! z*@}^E%NhuS7l9Hi4*$AOf3gMHlo6{}h8o~$ND+~a;N-^T;BBBJQGm0PLL1D_uMzL1wToaBIAPy9qyG()5&bjspARE(d`)>-Rel z(aD@*Sn4dlH#2-!HYOO)U`)IK&It;w*w%F?R5bg(UIwblWrq-*@;d=-m}Be#12#yu zF#JCINbVCawb*yqn4dC2zHeohp7fls_XR3-cyo7?l(wq>y83T*nTXC|HLFbG($BIW z1#gv0pfCCa>-wOwJ{8PPQUC`wY--i2bdWNdN{6>K7GZgy0-Y?7~4 zZduCaBKIA+O-<1Xb2jEMX);mwHVj$GJETW}v7=a4ZNA6d*vd?H{p~GrP`A(Cf&?o) z&bUx^HeICkRPI?52Jh2!3Px@4SwJ%`T;((5Ky!F@b~EqaYLhJKe?=xWz5n&%!cL=j zGXMPq+!_)OcTRcQkC@^n*y@#l1wn#{`p4q*-iybRL$m`FmQg#SmUHbSs6{s z-g_FFy-9S+61VnP*&=#>mbwP9-L|0Ps`1c#S|Xd}ZYNVnQf>zpb+TUTIB$r)%qPB8 z5u;7BM2T)sYXLAOR|8A&$qw0CiSIKKqKlJ2S3Dr*$EtN^)ss0!ToGL+U;LXWAmdE- znIDLiRJ1zBGerP0Svr7%KX8klB*Kp`ogE-D?w7mn<7_Z&bX`_I5S;8a=P7@w{s0qB zzAk!WheCP>=k`}kh=(eq(J#r(77B%wIl)~2SP(N!WW8*J^$u~B#<`BGDN{RE9z^Sp zC#*44f|xOS9CNfP$2q2#yQ3^z1R%n}H|Y=^X3Ub{VU|RVCS3?6w};LQ1E#d!L-Y+5 zM&-TUjPnkovKiT?>9!OgSM2ON#>7kMXgcUYsKxk>4kngT+p6QKd=2&)uWPG#hFgXi zE2Pd46n2Q6EM(h`ng2TlPDS9yaw}8^%4B3LQ=;x>Sq_tA+F`xD+lUphYW=wh5;dn! zJpvAuNo-4=@@j!?DeW5+XJJ-0ndbW=@D=|Db0cgB6c2N~Z$TXI$waz;bGl zBye`bh4gp2uy+bYEkumA1NDj@iW&sO_I(yx>7%K>x{uwQExSCF0;xm4`St-S zp>4Ab4!OWDT~Df1;5kxlPR%5UhTV!H4fM9-(l+P`$hGYfE)6^s;;Vx83Fx-<92X6G z{igktcRC`q9viu)5YC3BzAQ#O-VTSp93_Cl2uY!#Lwd@>MzAI2Dlav-O31bN1>41ik@I zfEhsHpMh8TQ|JrjSE2pF(V+AoH$Wc%9RU3X1Ondy)xx%YTY*QPp#X;;?VsOo4ZBHM z%rC@`!Yh77|LrRTV80PR8j}!D4V3>p`Kx@P0ONs**U;ZnUzOhjbIO@_FT}3{Q-MI> z@)zY#I>&xmx`Av8)=qG6LEB6)riyauY9rOZ!1_WFJTiE}&lYt58dk~|x z4}2!vfT~b}?}TrCadrkxJX(CLXu|HS|Af`I9^;Sxyc@#EHhjomzZwkTl;KqY>xM1j z#9oJG<+CPQOn1M%-PL^;TU9VfIw5cq7I@TYQCPJ9Dca3_Kg}Y$qMSRo!Q?zfj8_31 za=3jvr@Q+qa3XX}VULvaeh)Zk4W}o9O(}a6*dsS%5te@>$xIVE^xs&)qra?*4m{vLuL@Hc+NeKoA9_tG%4Rik_PAeV&5~{I7a>kpn{Uupo60rRyw|AI1apBt!=2 zIm6_!bql+)E+4C=?->u@MdqqM4p{FdA~;=z|4iKl*ti7z$vq#b2R>jQR6e6v2o*(4)d$^x#lq z(u-k=s12u$o{gGO1e7~hG&Qv}U1M-;EAsqU)Z8#S)HlGvx zrd$lYll()H)_I@#(n@TNY4^^=C+Vzg{RqmM?9P8YdqjaVO@yPsILf=;_GhTy#SRdHy7_ksA?@Dv^9> z{#VV|xVD92V3+m}R z!2{L2?T|Lz9gaczqR$y;(f)S}fZ)RVg;LCM@7Zx%p24j#vy1o1r<2v9eF2-IlFYM$ zcJtp8KNc#4>Hjgth#`605jRYs{vB}E0Q~Bd`7cuvK*w~*5SDUcX@?nD;>1~!N( z{INih?SMz)S3~Mstxf-XZZ$+<@KL%<9L|1;TnGt{uI47o3cjf}`b8x$D4pXqPSVao z*6!QlmOy^Ku=^v#+Xr-zk-G))?2T!{!20s?9y261IMgZuF~$ydmHQoL2=g>%Oe!AY zxqeIPcF+M+2{Oz^~M8ZddAJvwIYYW?YxH^oU^+>SctBARW(wk-e z^k|3Gc3b7;G;&87L`a4Ta@rQW3DZDmtD5#4b6_U+3mr0Dkoe)3CE4pBadPKrvP}(| z=R`^+hV^nynhFI;JtUdRi;>jRQcLTH3la6FQ<7cTFss!{YY{o{FU@eUBLfscddDbC zcGqaQ!#BAVnh{2(2HE)&bn1zBQy|1M&=Q|I1g^sE*U>0$lbEDp>3w#;OIkRtNiJ?r zv6>RYwiP08Iqpm|Q;J%$H?m~OZ=})HzQWOUFjc5MO@R@u>Kaa8!_+aS@kj?}%yu)G z-md_@fZzFy0vA9lxkBi>NhVfpF#0JX7j7RRYCH*ARs>w@=SOq?{9Wq%Bu$7TtoZxp!n0xEN!nT$#p|YK11ob@t9(i zkL7R)PHuww6QSG zp+h?*naT-34dLBcQ*@&!RU+wy?!7f0Fgs0=t5P8ghk&Qdvx_z3-MHRCm3DjEemzr# z0xlCfs7B~pFA~qrWk3Z zJ%j8?ZG~j^(xy^`&SX{7@I}>#UY3$h-8lY)O6qpfEyKr{yady-eB&QAnhA_}*AjJ> z+8ARCqw5A2R;mRm+jrz3?eUWWSRfeO3pYRV^weHwvCljB~X7RgDM8YY4)BW z1hSep@D+8|WcE*SmEzP>Ud`yg30_1!_+VW+EN{t*8}7!e=C6A0u1Hl(y{w#F8kem< zbG!S{z}K$o2C^*i^=&Nraoc4M#+e~H-Dq)z4Bkapaa7XTvE-EUHo*}BEnEB%UYDW$ zRt}aYU`4&MnZ+`dWWsGTrY70Z8pRc^eu&Z*S-(3vFQIH*jLvXlZLg%JOV5k6X^N-( z6W3vt*E(g7H=jag?H>~(s=#0Coj5{ub&Xt)Br=~_AYh7<)^<5w1%D8$%+K&c@O`Z; zaopA1o{gwFpXEkr%FsGCA`>cUEu6XX%V0JIo27kpLCynH=+0logXAA(z$JFOTTA!5 z1rU4S2JHnf{az3Yk$W;aG`kZ!Sr8_Ac}PhJR){fA8{Y{}KyZ}W8Bgq4%J)NSvMq8` zL?%UuN~3xx!WWaTsisk*VyGM6_vgS>iITX zSj5Y@a$1n*$53ss-NV)mMO_zb28@IahrrG6;dZ*foJZ{abieDKS{_hPX087Dff)bg zO6xeLvvHB;N`aqavXKS2Zn)>Y6hKmO_Z17NWHd0-hRgx?`%x=-tQUaJ)4aQ3nK^fPM9yn$)(?=rWGi_o6aKD-*CE^{pirVco##?R`p!EFwaASI z{cP2=Lm>JXHkHgF{bQ5>8O1F^Tg$C8lAylpo}nr+7-&KG$&x^P4pP580AIY#$96H* za!P3bU{;FVU>K{CIvD8$h6;BmY~jmRG_~#;2u7F~Zv4o~ocw&0vV2p2EnsiWwD6%; zU~5g^s&m+*J%X==6(W`SA!t|b;eD0pPMF*IF@-0o9laH|8?q<5%i{Gi%{_||VNiV$ zOY}ML-sd^>&@jJU8r~{$ z_`Sn9>#xm*61D{#2j#nC(N4Py$EA2^DD>>GVB)QQ`s;t2C>GVC1Q~u7+Un0)Py>;; zzym)hzmg3k0Q*Nsf6xI8F&3h^Kq?r5?%EUYPXAsg1D&6OaCa+yw6R>T)vvZ>ldI$D zJP5I|u2Weplu~Eef3Hek+k@u)|2*fTpv3Qlgb5~x5rU>WP1r)|zg4S|x2$c+0q%rq zyQUdrLkIFmHqoc(SVJlR!bhwvb|_!lVcXqqsJ(2dqZ7SQ=oo5+Vv4^De~VSmPv{qp z>E^rrDG^XY5xAozGgbvJi^z#BZ~baJ?HT;&_h&a$jUU#Eu|5=cB>Ch!eH^PUaFN$R z^mB7{d=j~-Qg-(ya9YND)}9@X+vM9%Vnb$FceszD{lE;ZQyPYbM2~IbPZInTkL~;m zTLR{x*gPamWB?=fKgH6Yk~E}q_pJ9>=|3kI5MkwpAmfa5V3<1YVoo$QZIpvUBrkQX znc;}0J*i?3IW0sKLLd1Y!63^-5d>}_T-Qk5l3!g*GR`@+wQ?Oj<)pnvg(S3G4XVYD z*)FHbI-%(?o~Yhp$32A?$*`nS0f+@E)NKU>%6y=%bt&!nqu~AYJx@kj*kwl%N2woI zxDnSN{WoQi+&NKmcdukt%at`IeY-ytN??QyTw+{<#>gbX_YIra7rDiEE>G%fx-K`_ zz4mAv6RgFa1;R{{LbdJPY^2>IoAvPh7!?eHZDi0G64ST|KTHC%&K%_#Uh*%bb`bcn zooY9Av)Vn%`uEVnv4R@P-fJB$d?NRpA}(>!a{#xLjIk6?4sw0Ax{j#yTJ!i+KEeZ5 zA8GORm+jPReu6JNW-+8(Qz0)G7~UmPQ=;+Is^klg2r()-U-eGOd#Tu)>npKU?f4m; zc+hZLlf)$qV{wRRaHfR>&6cgyi(Y?z)lvBSzcf;(PP60;lINdCl3Byxa>5ac59PnK zmS1Y*ezg1G!m!&%8yNbF|XwPf<|>yaKExzEeBr}t@qVQ`EzkK4*yFV0}d9*HhpIBgeqy_o?jIGFbw~CJMa4Y2qvRS#9+G! zS&)RCm$`N+J7`+7B1^9Fuk9+_tu(4A1p7;;)DyZiJD|NJ;!*?8cP%_NBgWG+WL}eQNn8)fcZ7pd)$;xOR zdmBoEWDxQ_-VzEv58rlurwO=rcpam7{Q*}}D{qZp2kdsOk2NxZeeF{*Z_u`6Yfq0(!^o}BAzYxB0x{P?2D_0B@ZRB9gUL?hO-kPvvQql=I3 zP%7-{3jKH>A9!53|8WpN+DA04zOG%PPW9J)CqFG{-I|FPgcYyTL*a{XQ4M9&wW|*ivZPs$@ORU% z&N2M?)#)<8+;3hl^|vEiAS!*kE7j_o`F_xdj>NiYi%ANs8=N!?oY|D!;FCgN&NIkjsAxF< zYu+HAWUv@YdVTm)YkzDZu~b)9r1e`v9v5ppkwSGhrl=Be4)u4KXQiyA-SO?Lo`!O_ zNuh{|OC^9T6rIC?ouv(=$*{pc4jMNIBN>7*NDJ%@uVSr~zxB&eqCz7eYNdgL@PpMtK8;Rdi@)kC6Jj9ZM-C=shqr=-7(=dtT1+=6a`5q8TcRQl^VC z<~pizBPIL(9!&S)8AD^$EJs%@evC0P?1XS)U6_-0Nt^oirYaH--@e?KJ?jqUJoPdH za5BZDgTOSzV`{x(p!6dJ?Q46l+3n&TY=bmNJ8H62WFao>AGRsq+{s`R)>L;Ij(zhq z+!KSfQ2Gl4Kxhnm>xgLK2R5qMN`#EzOkicDcMu9?_(sY^5F!)2XK-o4KB8ZyavqmU z&qz)j)4uKdQ-z3ZX~wen$<|?KeDJ0~zpV1Oj1~gtH! zePCQ5(A@>MzsX975^bCtDyLO=?%w>@of)`5`R{S=jqb#5Qu|s|lJO}l>)ZbuO%_S_ zQ;cKp6+n3t!M210i;?y^}sigG*or$aL& zvbpm#U=}X3f_%zK$nu$&Rx&(H)i9*<MMA&{VKVDJx5f~tl`xAo~gohI|Pq`0X1B4}aRHdF5=We39n8jv8JFdM?he?{C@=##97kNc-azK9mk zP2p>_8qs6$;cnb>{1#Q+>M>!&wXX@aL#0e>ecR9ubVEZi$syu%hcD1RC< zqf`6E5sVO$MXki8h=+Z*yb7DnI>UqMVIXXi^~fc&2mKNr_K)n>{r{Rw{5_E%7BNdr|ncH0JOA;3qU z42iHPM!qD3t`PYXUhWyWXYm6r$puH4itCerPPBYfOxo2|WWg_kO1*^l0MXE4RT|GN zbFClo2K9gkq*Z~Xy5Ne-{233=L!FI`O5Qih>Q8r`&g5e_f#T^HC917sGZO)c9AsAw zcujfxVe0HGf#*hkosobnjm|Ccdfm%A@k${oPv(7zF9Lc8z7mZP}!w^gfpY>KN9NW^OQjqZ_Nw(9# zCQK}DVVQ3m*zdUBuFIJ$2TB5Gh}t zV13B6#QE_q4Q6XW`#xUQ`!SZz#&Zp z-no_$Sn6|h5vdirupEDmbBRxvD@Ci0SR-~(`$Q>U@`kATygXkAa%_e^A5o_H>}Q8I z4;^|^e8cZ$z@N+TE+QgV>pSg8vcK9Aw#HtGwb~S&E$El{6&H3C?JYVQd22uNzH&)MpAE;XnhKX#*dtJe}=RaJ)c55{t8;z%m!xQPsI_eEUcglUzDBF z;QOdJ{=p~UP@+u>m_~wYSn(5eYW1cXVGLHC0v9{`YUkUnPk;cw3xChuhR&ax3!C8^ z8QLqz8E)Kous~D4eX+=D%Hal*pU@tf8C3lGYT@KJ$&Ev63rbg3Src4vEJ93IY@a%G|`3?U>g-K)gvfK;3tT}Zzb@@fgirde*5NNbctmLD9F`?!~_@pk3 zBlyo(%$o?esNu@xkRSOtMg+_Bl|&=4mS=%IVH`v^6zzqdu50Y8YB~W6e?8V+?nP=$ z9>9ZIYQ$nC^x>*0Y3az5y9`>xk8n(+PpOf=hT+g^1A9ICct6ZSP|eV0nk8cv2+XufuTtW5q2yd_ zM`hp+tmL=B55z8P)fY(+bf0z=Y6)F^y?SC~xO>w%>t($%r@jy^k5Gm)B@ORG3zFTA z<~ZONP8vQ^bgt-oB(9IwmmCYsMe%K3`h@7tj&8*f3?!3eHU3kkT2<>@t^4kQ4WZ41pGX|rkm&0xhbGT9oQf3P9Ap1c^UWUDoZFg z=KW!D3I5eGRE5H(vm?P&(J;DLPzk}O*G5v)uRAuL1-`f;meCY8{XuO4_iH?4`al{h2z^bx*o~e!b zPE8?Q@sFN$-MDj-z8Z5`yGJ)-gCdm_R5vmor(UrdxhYRm;a@D=s4HIi9y;ItWJd3Z<865^B}f*&VHdH)Ovbw{Uj&!nGk9%j^u& zFGpneDiQVmvo#SJ+Bcbv1i|gO?O`-gA$|5FjAvY4%nsTG_hFjMW`E=STxoFbsz6B~ zvlGJxC97KsTgEV-JKOTs*RNtiEX3RZ) z%b|)s2TLKYt)F~&5+sU$lKCG|UhOl;{71xpmk&ehEh9a}0E!d`N6hj0|C25jZ!y(d z{x)6fb&f6z?{lryFGTsNYzG(gPjmV*#8q#broGg^0_h$m(q+cE{^jwEYwr7yo?pMS zuD{902fmZfr95CH)+kB#rhy9mJyY4&ZIIRTL>H0CD(3x^wmXL#$bB@fa=y5B6grei zhqg1jjR2O^?7xduXRC|<;}ohxo+Eb zRYmyaQwGp+)SI8%oE>6=Kr>_Q=i0H>!*=W}faF8|BddJ|<29bx_%T4XJNWeu=JRQF zeNBnbTyH#W<=Gp%V{VY$s?`@D2cc~DC)Z@r@IQN{vOFG&;_{`?(Dh|%^8r$#2YvSk z<>u%tzN)_E5;2;EdWu+0N{_2`mslZeZWCJ?3xO8S`yfH;7mVkX@tq z15bowWC_}q1Q3i_v#?~^TQ2Rx%vNV$m%p5}G@-4!dp9 zx21OLPoI@O7U0P_)AVafso>G$a!$z8i)x6v406X1R#|JTCh_Q_`K2W6;Q7eM`z30i%{P7xl#7P9I){yN$m&j!vL1wv6!>2tDi5iyLKjGD&t3~Hvn41=^@{bj-10Icu{-ma(}$j@EY^{No(xLr^RW2D=ku{U@EBI#Lu3h=V?2LS4sS1O~QWC zkoM~COh5XW1OGX1X<49dC#}J;P`U5D zhe_jRIPnmbld466D(nNaS#BvHrzM;@)vPa#OGmsKk1|kY{I~_iB|opLcb8pe?zL1b zZ-NrFv?sXxl!dilwCTe*pqfD4-FkKiE|6?Q2AzP0%*vcG$@~ltlT{1iDQHgyqb;o2;bMRks|V6~8~UlDHPATIV9Y06*51UKW}5g1#TwSVg9T;@$4mH zOdGcnXMVr7zckg}R;;m`h$K1=S1cGPLM(<%b-LXptxiH3bMStL02xp2;K;1p5b)wT zAYeHc1hl=)!qa+@2MwgUUm{H%D2UDWnJrW(*cAW9$twwset!B;QZ_`!zcAvUE5J~2 zU82{O=4@{wCblteyFyN+Fip8%aN$iaLG^o7n2l;p1t2_44V!M&z{#jzVH zX6ldUiQ3)B&MQwZD9H7s$BG*~sT6&_;NMb`wW+np( zq|9@L6Xrl(bA*S(A{~1`DCoE8gU>sn!lRckp7=8shiU$ltJqcYmvv#TF_I(?V?vA< zN&Xg$ShJ^MvOK5Ehs_zc*A)NOPl@FV$J%(*iy~L=h_% zJ8cmS0*6s$7(4~m)8}D&3y!t4Gv#OC_E;T^rIm}|!I%9i9yb{d3U*pVADVsu;2Ps= zxS=*g7TG&am*}m~w07|)4FiMe#_+Yh{<)JjTKOSCZ2w&E0O-<;o2X?R5Xf|; zcdJ$Fu4Vm{R@EqRu*FzS4x{at@U|&sC$ZzA!;(Z+T;8&0y*PV41D;UjVZ<7^Le2<4 zK4$%1M{mm1E}oGDT1J&V`qnvM>mlDL*LUYaNFrH2-rV0eot3H<8lKH~yGWcrJ708z zd&rqe9kzW^HPyiiSF209AB&GVz!`ylM~T&3X(aB+GVEY8gNd+1MXJWKhc2jKy-*qOS;l#0u*)l2o=bPk4{ zJE-O14)$A3-mH9N)_;Q0Gy<003}fThs?vvGFXWa$&D<9ta8JAl1Y{TlXTUj6I=1|e z?Sq(%k2drfzqQ7CKU2~7ksskLOjDHsVabXvQjLPM1}5_iMm=9&=?HgvmJW2bU^OqO zO*Ya^bSIc`;7|n0N?hK$-6~9`V;Z1@d2*EJ0eSPB(B`AhYjpG`CMhPRmgUT=qNN(b zk}ObKpx<93?}BYdyJp!J$_#;mMj1r(y(Eic2|^=)QSx_xx|PIXAmD8{w3I3C>8zw2 z%IJd(JU#|HxrimpPirKaNbhT1;}^xRHSNe{!+}bJFb}lAGu?4YLslb_F(qqmt@r0~ zwLUmFS= zPfEM>jVX^vKSMFPd-#FpMT+k*8a2`6s2Z3Mg{ZisvwNzWnsi2`6KzOTG$})cil&=& z6AXd6V5yQAI0DhJ=2_fm0Q@R^OAa= zBz%#LC3)4oM)Nn7QlE-gh|!Ix`jW{rJc{MrL}K2znv)%mTNb=6aJ_ z1MtR7Vu6&!`E(}m?^2)P_6Lzd%zF~@#a+hiTuRY&)tGgMko+e zJCha{8Ar{MeZS_O6f)R;XyW{NWE(h`Ty6wrDy+YuKS~wFd*1| z(ARQ*dOnSAeJP+!Dtk&dDLVUdGi1~QHkB0)x97>|TxuW+QVzTpj2oPscPwLcZ)EXW z4gB?sf(Q(Nk>*^r3Grk!ZL>&xHP39nLy{R2#qN#6Hvu%g&*uyHslnl}NztL7YdL*> zPY?#R-MZI85MG{d+ zrj8+)9C;eyj@LvHpy4x>ty9xKBzga95eC$C<;}*~-05`Lh0WF|kn%Y+!9;E)3*Sl3 z}{3H$dBxJ5E^i0)y-RNw;~22YB+!)#kXj^MP>nV9pBpKUL9u!s19}y1MC8 zjlVc6%orNY4xTGbN7b_7AA^eW6kve}BNoVuAvNQ5Q~A80k_-f;+i?AQ^V)w&kjRtI zpnt6L-BcXcsa6T${1ZTwL$av_J_a916;B-KA-VP($8_$5J3?y8l@iI!VR~N55K2|T z-tHLQ%iYN_Im>M6HriH7yS8Z(d1(T;2d|3I4YYj zzy->t(p&4$nHuI%A|w`w)YyXnX|%WYM9442rF%?TX%T-x80!E9Di~`*cB#RM#i$Q} zNz)kdB(gN#)`+`@HKnTn{=zlm8D=$486d{e0`7}aX%}U~se+MCYGNcaVopWO%GcHe z)ue!jwog8%6`Tv+oFQ-(z~<~m_&DI{jzqbf4SGRI8vENO;3T8L4rHmb;`-Ib%jJB= zy6Fv8qoA~=U|op8fEeMM+etvGdyhL6i1N6C*Xh|_FDb%4XFXTa5teMmv&E1=eHWI& z58tsw&&*tw^J+HLd9i%tn>QF-Ku)x5|K*8Ia0i=@A#e2k`c=e58Con}*2H}ig|!)? z_f*wMWZ=9@zH<7&865qG7bSgYvs;nB&q*5o2lvYz-b7(HuMY$7DKsCFH-1`|0ep3* z6}NNCK;Uo7IgLdq{pf1sYQl7jlOSa2*w?5_af#e_$G^Jnu~r7u?ztrh7#c=czf{L& z7fSf@=>duw5la8BCiu_;yVVp;p8*jA*m==qB@5N*6r&OkdzV9(gWyBw6NKVpFj*KV z{tvO+H>ItDK#k87fbN7HGON3>Wc=59r;hUCBjkwh_N75x8&i*J*>GIw7!@2_mdjBA zC+An>lP{_kT5T@L3Wfm>?Zb*f#x*A)&orD(sQ05A?)%?8#2+{$>*K1zMl>l?|(swhW}&x;eU zN{<8&zm4mKwc;uHl*KBZf-OuHNUDe>$xp*aWt#qcXALmITsSn}(lZfh5^`0OVI}=% zd}&@Cn@ET%Jut%h*L1b$_XncdR)W~{wE5bS%ZZzzQ-8_4nLc5CWADfdrfU*JWN`<> ztrs)$Psc1$41u+^F06^dTB(`_43MY&*tX`_x1=HU!=36b{n4)TTosU`V>{f*e+;0>G-W-L+Ew zHf1~6qJV&4f=G~g4KrC}8W^+I5@ikv0NS{(|5ZgCwogeiGdeP7*u0kgrTF&$Q_!y}Aa!$R}0Y}a-50Ym_FSWx_#o#D4cGjV??$E(orBg)Kl6kdD;qU@{0$b7i zUNl05u78WMGxb>L6*a0k8feFZNhpP(^&5>>Xy1i;RgxFyM9~_!7r5;@*|rXyVa%e% zst&i66YrSsv-s6x>|w5-n|?D)pvHl5r*|Z{Dvy&VDR)+%@U$y82|Rt{OqR zICeHv0#k6fc&_{go4E|b%LUG8{MqRvq-LJaT64Y9r@7Wu5BAC;ii~vpwpq!Po>O3` z`OL73^4u6)y8QV)11c=h#bf=Gu~$?`^n(HtGhfSb+_g}CJP7c#mhs8_A%k0tqC$K$ z03qmjx`)4*6!GtH-agi@BEq~^5YTxrIWXbFF`!aI>bu;<_0Kyu>`}!3{u8#X$z%qz zQvUDZoo%k`!7p;5F5Cv;EU?0AkvG0qG;+_LI|$l{stKrZRjk(5XA~D1tR6>X^k1VN zceC8A8@VX4tDoEIZRbtRmtxoTa&M2E{*{Q^A8nH7JQhAo$jKcufAyzAzIzf*qJ@g+ z>`y5vsg(tYyp^gWf$MBanCIfs|I_{eAsQPBqKN9`7dGh8EE83WE1AK-F&n`Z8k#y@ z1;nA2xw?KW8S#D=XA4*}K`b5VJ2wR{E*D0p=s0np)SvG>;y%2Vf&l@&MD*jbw$v} zBkbp9?I_~yS|KJ~0SHGkltv&0q(PJgC=@l=+*HAL_r(*|)|2Tbh&-WKKL$+OO~K3V zbO)UFte3O_^UX8dc112s+(n;|Ccosq6PJ5k?)RIz4G8r@txL^NhTo|IZjBGh$`g!9 z-O$f|t_<#(pI^DlCef{b#hC0{6p@^pSkrmlLG5QoMpG-fU$u?iDw!Y81??1F+!?%{ z+44ksUwCHDAuEb*vl0=+llSb@iY7)Bpiz4$`et5#yd}8iP?UDQ9@g1$nERARGnLrS8qh zv0`yUA{DbN`n0CJI3zt)4Y|`2>~k_(t=fSxgOmX%TTU3)3p+4w!e3s`RqIF1MwLhX zW^Ya&kS&s4MjQGB(rKd~`5i(J)|?++}CCtassYWK$Y|7txK=B64Q5(w+e7&pn;CEh6zzF6#x1=Ng{C( z>W4(-lOtjth_>(C3;S1m;?z0r$r}TXdj|*~Q zFDcD%8z`Rmk|o19*{Nv#Tn$)87X&0$>x=aKIf!suti%Q-C8t>, - private val maxApyStore: RuntimeStateStore, private val networkFactory: NetworkFactory, excludedBlockchains: ExcludedBlockchains, ) : MarketsTokenRepository { @@ -95,8 +94,6 @@ internal class DefaultMarketsTokenRepository( val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res) - maxApyStore.store(tokenMarketListWithMaxApy.maxApy) - return BatchFetchResult.Success( data = tokenMarketListWithMaxApy.tokens, last = last, @@ -294,8 +291,24 @@ internal class DefaultMarketsTokenRepository( } } - override suspend fun getMaxApy(): Flow { - return maxApyStore.get() + override suspend fun showYieldModePromo( + appCurrency: AppCurrency, + interval: TokenMarketListConfig.Interval, + ): Boolean = try { + val hasYieldSupplyTokens = marketsApi.getCoinsList( + currency = appCurrency.code, + interval = interval.toRequestParam(), + order = TokenMarketListConfig.Order.YieldSupply.toRequestParam(), + offset = 0, + limit = 40, + timestamp = null, + search = null, + ).getOrThrow().tokens.isNotEmpty() + + hasYieldSupplyTokens + } catch (error: Exception) { + Timber.e(error) + false } inline fun catchListErrorAndSendEvent(block: () -> T): T { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index e280e160e1..7dce907fe8 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -16,6 +16,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) TokenMarketListConfig.Order.TopGainers -> "gainers" TokenMarketListConfig.Order.TopLosers -> "losers" TokenMarketListConfig.Order.Staking -> "staking" + TokenMarketListConfig.Order.YieldSupply -> "yield" } internal fun PriceChangeInterval.toRequestParam(): String = when (this) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 067f886293..ed6cb59dc4 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -40,7 +40,7 @@ internal object TokenMarketListConverter : Converter { + override fun isMarketsYieldSupplyNotificationHideClicked(): Flow { return appPreferencesStore.get( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, default = false, ) } - override suspend fun setMarketsStakingNotificationHideClicked() { + override suspend fun setMarketsYieldSupplyNotificationHideClicked() { appPreferencesStore.store( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, value = true, ) } diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index 36b179897a..3deeee528d 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -12,7 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, - val stakingRate: BigDecimal?, + val yieldRate: BigDecimal?, val updateTimestamp: Long?, private val imageHost: String, ) { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index 011df2c630..ffd82b8909 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -8,7 +8,7 @@ data class TokenMarketListConfig( ) { enum class Order { - ByRating, Trending, Buyers, TopGainers, TopLosers, Staking + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking, YieldSupply, } enum class Interval { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt deleted file mode 100644 index 4ccaea6680..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.markets - -import android.icu.util.Calendar -import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.settings.repositories.SettingsRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import java.math.BigDecimal - -class GetStakingNotificationMaxApyUseCase( - private val settingsRepository: SettingsRepository, - private val promoRepository: PromoRepository, - private val marketsTokenRepository: MarketsTokenRepository, -) { - - suspend operator fun invoke(): Flow { - val hideClickedFlow = promoRepository.isMarketsStakingNotificationHideClicked() - val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate() - val currentDate = Calendar.getInstance().timeInMillis - - return combine( - flow = hideClickedFlow, - flow2 = marketsTokenRepository.getMaxApy(), - ) { hideClicked, maxApy -> - val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) { - currentDate - walletFirstUsageDate > TWO_WEEKS_IN_MILLIS - } else { - false - } - - maxApy.takeIf { showStakingNotification } - } - } - - private companion object { - const val TWO_WEEKS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L - } -} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt new file mode 100644 index 0000000000..599e7cefc3 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.promo.PromoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ShouldShowYieldModeMarketPromoUseCase( + private val promoRepository: PromoRepository, + private val marketsTokenRepository: MarketsTokenRepository, +) { + + operator fun invoke(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Flow { + val hideClickedFlow = promoRepository.isMarketsYieldSupplyNotificationHideClicked() + + return hideClickedFlow.map { hideClicked -> + !hideClicked && marketsTokenRepository.showYieldModePromo( + appCurrency = appCurrency, + interval = interval, + ) + } + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index d813fdcc17..00a0e86540 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -1,11 +1,10 @@ package com.tangem.domain.markets.repositories +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal interface MarketsTokenRepository { @@ -56,5 +55,5 @@ interface MarketsTokenRepository { */ suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List - suspend fun getMaxApy(): Flow + suspend fun showYieldModePromo(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Boolean } \ No newline at end of file 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 6e7afbdb02..de65de82b0 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,9 +16,9 @@ interface PromoRepository { suspend fun setNeverToShowTokenPromo(promoId: PromoId) - fun isMarketsStakingNotificationHideClicked(): Flow + fun isMarketsYieldSupplyNotificationHideClicked(): Flow - suspend fun setMarketsStakingNotificationHideClicked() + suspend fun setMarketsYieldSupplyNotificationHideClicked() suspend fun isMoonpayPromoActive(): Boolean // endregion diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt index b513310fb1..6bc58aab6e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter( trendType = value.getTrendType(), chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, - stakingRate = value.stakingRate?.format { percent() }?.let { + stakingRate = value.yieldRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index ee02fa8424..6b7fb68da4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -338,6 +338,7 @@ internal class FeedComponentModel @Inject constructor( SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt index 7fa17ca9f5..b053465040 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt @@ -386,6 +386,7 @@ internal class FeedMarketsBatchFlowManager( TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking + TokenMarketListConfig.Order.YieldSupply -> SortByTypeUM.YieldSupply } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt index 27e1c0daaf..88dc1834e7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt @@ -41,6 +41,7 @@ enum class SortByTypeUM(val text: TextReference) { TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), Staking(resourceReference(R.string.common_staking)), + YieldSupply(resourceReference(R.string.markets_sort_by_yield_mode_title)), } @Immutable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt index c5f92d0700..996e95d31b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -24,6 +24,7 @@ internal sealed class MarketsListAnalyticsEvent( SortByTypeUM.TopGainers -> "Gainers" SortByTypeUM.TopLosers -> "Losers" SortByTypeUM.Staking -> "Staking" + SortByTypeUM.YieldSupply -> "Yield Supply" }, "Period" to when (interval) { MarketsListUM.TrendInterval.H24 -> "24h" @@ -32,12 +33,11 @@ internal sealed class MarketsListAnalyticsEvent( }, ), ) + class YieldModePromoShown : MarketsListAnalyticsEvent(event = "Notice - Yield Mode Promo") - class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + class YieldModePromoClosed : MarketsListAnalyticsEvent(event = "Yield Mode Promo Closed") - class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") - - class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + class YieldModeMoreInfoClicked : MarketsListAnalyticsEvent(event = "Yield Mode More Info") data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( event = "Token Searched", diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 600133bc7c..e214f30658 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -10,19 +10,20 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase +import com.tangem.domain.markets.ShouldShowYieldModeMarketPromoUseCase import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -31,7 +32,6 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.math.BigDecimal import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + shouldShowYieldModeMarketPromoUseCase: ShouldShowYieldModeMarketPromoUseCase, private val promoRepository: PromoRepository, private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @@ -69,8 +69,6 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, - onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) }, - onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, ) @@ -112,53 +110,62 @@ internal class MarketsListModel @Inject constructor( marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> if (isInSearchMode) { combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow5 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = isSearchNotFoundState, - stakingNotificationMaxApy = stakingMaxApy, + shouldShowYieldModePromo = isYieldModePromo, userCountry = userCountry, ) } } else { combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + flow = mainMarketsListManager.uiItems, + flow2 = mainMarketsListManager.isInInitialLoadingErrorState, + flow3 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow4 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = false, - stakingNotificationMaxApy = stakingNotificationMaxApy, + shouldShowYieldModePromo = shouldShowYieldModePromo, userCountry = userCountry, ) } } }.collect { marketsItemsData -> - val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() - } - - if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && - stakingNotificationMaxApy != null - ) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown()) + val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo + if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown()) } marketsListUMStateManager.onUiItemsChanged( uiItems = marketsItemsData.items, isInErrorState = marketsItemsData.isInErrorState, isSearchNotFound = marketsItemsData.isSearchNotFound, - stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + marketsNotificationUM = if (shouldShowYieldModePromo) { + MarketsNotificationUM.YieldSupplyPromo( + onClick = { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModeMoreInfoClicked()) + marketsListUMStateManager.selectedSortByType = SortByTypeUM.YieldSupply + }, + onCloseClick = { onYieldModeNotificationCloseClick() }, + ) + } else { + null }, ) } @@ -266,6 +273,14 @@ internal class MarketsListModel @Inject constructor( mainMarketsListManager.reload() } + fun TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval { + return when (this) { + TrendInterval.H24 -> TokenMarketListConfig.Interval.H24 + TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK + TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH + } + } + private fun initAnalytics() { containerBottomSheetState.onEach { bottomSheetState -> if (bottomSheetState == BottomSheetState.EXPANDED) { @@ -302,10 +317,10 @@ internal class MarketsListModel @Inject constructor( }.saveIn(updateQuotesJob) } - private fun onStakingNotificationCloseClick() { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed()) + private fun onYieldModeNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoClosed()) modelScope.launch { - promoRepository.setMarketsStakingNotificationHideClicked() + promoRepository.setMarketsYieldSupplyNotificationHideClicked() } } @@ -313,7 +328,7 @@ internal class MarketsListModel @Inject constructor( val items: ImmutableList, val isInErrorState: Boolean, val isSearchNotFound: Boolean, - val stakingNotificationMaxApy: BigDecimal?, + val shouldShowYieldModePromo: Boolean, val userCountry: Either, ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt new file mode 100644 index 0000000000..5870289d78 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.tokenlist.impl.model + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +internal sealed class MarketsNotificationUM(val config: NotificationConfig) { + + data class YieldSupplyPromo( + val onClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : MarketsNotificationUM( + config = NotificationConfig( + iconResId = R.drawable.img_yield_supply_in_market_notification, + title = resourceReference(R.string.markets_yield_supply_banner_title), + subtitle = TextReference.EMPTY, + onClick = onClick, + onCloseClick = onCloseClick, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index ec31aac834..2e0926eaf5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter( trendType = value.getTrendType(), chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, - stakingRate = value.stakingRate?.format { percent() }?.let { + stakingRate = value.yieldRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index 03b8b24eeb..4847ff333a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -343,6 +343,7 @@ internal class MarketsListBatchFlowManager( SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index f2c6ee64d3..33f89173a9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.state.* import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import java.math.BigDecimal @Stable @Suppress("LongParameterList") @@ -26,8 +26,6 @@ internal class MarketsListUMStateManager( private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, - private val onStakingNotificationClick: () -> Unit, - private val onStakingNotificationCloseClick: () -> Unit, private val onShowTokensUnder100kClicked: () -> Unit, ) { @@ -92,25 +90,25 @@ internal class MarketsListUMStateManager( isInErrorState: Boolean, isSearchNotFound: Boolean, uiItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ) { - state.update { + state.update { currentState -> when { isInErrorState -> { - it.copy( + currentState.copy( list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked), ) } isSearchNotFound -> { - it.copy(list = ListUM.SearchNothingFound) + currentState.copy(list = ListUM.SearchNothingFound) } uiItems.isEmpty() -> { - it.copy(list = ListUM.Loading) + currentState.copy(list = ListUM.Loading) } else -> { - it.updateItems( + currentState.updateItems( newItems = uiItems, - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } } @@ -119,7 +117,7 @@ internal class MarketsListUMStateManager( private fun MarketsListUM.updateItems( newItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ): MarketsListUM { val currentState = this @@ -131,7 +129,7 @@ internal class MarketsListUMStateManager( .copy( showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), ), - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } @@ -225,12 +223,7 @@ internal class MarketsListUMStateManager( onOptionClicked = ::onBottomSheetOptionClicked, ), ), - stakingNotificationMaxApy = null, - onStakingNotificationClick = { - onStakingNotificationClick() - selectedSortByType = SortByTypeUM.Staking - }, - onStakingNotificationCloseClick = onStakingNotificationCloseClick, + marketsNotificationUM = null, ) private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 5b80958c46..0930d3c1e4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -38,19 +37,17 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet -import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification +import com.tangem.features.markets.tokenlist.impl.ui.components.YieldSupplyInMarketsPromoNotification import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM @@ -58,7 +55,6 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal private const val SHOW_MORE_KEY = "privacyPolicy" @@ -180,38 +176,38 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif ) } + val marketsNotification = state.marketsNotificationUM AnimatedVisibility( state.isInSearchMode.not() && - state.stakingNotificationMaxApy != null && - state.selectedSortBy != SortByTypeUM.Staking, + state.selectedSortBy != SortByTypeUM.YieldSupply, ) { val showMore = stringResourceSafe(R.string.common_show_more) - val description = stringResourceSafe( - R.string.markets_staking_banner_description_placeholder, - showMore, - ) - val clickableDescription = buildAnnotatedString { - append(description.substringBefore(showMore)) + when (marketsNotification) { + is MarketsNotificationUM.YieldSupplyPromo -> { + val description = stringResourceSafe( + R.string.markets_yield_supply_banner_description, + showMore, + ) - pushStringAnnotation(SHOW_MORE_KEY, "") - appendColored(showMore, TangemTheme.colors.text.accent) - pop() + val clickableDescription = annotatedReference { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + YieldSupplyInMarketsPromoNotification( + config = marketsNotification.config.copy( + subtitle = clickableDescription, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) + } + else -> { /* no-op */ + } } - - StakingInMarketsPromoNotification( - config = NotificationConfig( - iconResId = R.drawable.img_staking_in_market_notification, - title = resourceReference( - R.string.markets_staking_banner_title, - wrappedList(state.stakingNotificationMaxApy.format { percent() }), - ), - subtitle = annotatedReference(clickableDescription), - onClick = state.onStakingNotificationClick, - onCloseClick = state.onStakingNotificationCloseClick, - ), - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) } } } @@ -420,9 +416,10 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), - stakingNotificationMaxApy = BigDecimal(0.12345), - onStakingNotificationClick = {}, - onStakingNotificationCloseClick = {}, + marketsNotificationUM = MarketsNotificationUM.YieldSupplyPromo( + onClick = {}, + onCloseClick = {}, + ), ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt new file mode 100644 index 0000000000..e2f16f2735 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt @@ -0,0 +1,147 @@ +package com.tangem.features.markets.tokenlist.impl.ui.components + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.notifications.CloseableIconButton +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +private val bgColor = Color(0x2684B0D7) +private val borderColor = Color(0x2684B0D7) + +@Composable +fun YieldSupplyInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + var textHeightDp by remember { mutableStateOf(0.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = borderColor, + ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(bgColor) + .clickable { config.onClick?.invoke() }, + ) { + PromoImage( + iconRes = config.iconResId, + modifier = Modifier.height(textHeightDp), + ) + PromoText( + title = config.title, + subtitle = config.subtitle, + onSizeChange = { textHeightDp = it }, + ) + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.secondary, + ) + } +} + +@Composable +private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .requiredWidth(80.dp) + .wrapContentHeight(Alignment.CenterVertically, unbounded = true), + ) + } +} + +@Composable +private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) { + val density = LocalDensity.current + + Box( + modifier = Modifier.onSizeChanged { + with(density) { onSizeChange(it.height.toDp()) } + }, + ) { + TextsBlock( + title = title, + subtitle = subtitle, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.CenterStart) + .padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp), + ) + } +} + +@Composable +private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val titleText = title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + SpacerH(height = TangemTheme.dimens.spacing2) + } + + Text( + text = subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.caption2, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun RingPromoNotification_Preview() { + TangemThemePreview { + YieldSupplyInMarketsPromoNotification( + config = NotificationConfig( + title = stringReference("Activate Yield Mode"), + subtitle = stringReference("Power up your assets while supplying them with instant access. Show more"), + iconResId = R.drawable.img_yield_supply_in_market_notification, + onCloseClick = { }, + ), + ) + } +} + +// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index da29b428d1..8404856a87 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import kotlinx.collections.immutable.ImmutableList -import java.math.BigDecimal internal data class MarketsListUM( val list: ListUM, @@ -19,9 +19,7 @@ internal data class MarketsListUM( val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, - val stakingNotificationMaxApy: BigDecimal?, - val onStakingNotificationClick: () -> Unit, - val onStakingNotificationCloseClick: () -> Unit, + val marketsNotificationUM: MarketsNotificationUM?, ) { val isInSearchMode get() = searchBar.isActive @@ -40,6 +38,7 @@ enum class SortByTypeUM(val text: TextReference) { TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), Staking(resourceReference(R.string.common_staking)), + YieldSupply(resourceReference(R.string.yield_module_earn_sheet_title)), } @Immutable From 54f0be3e6c60d50116c334590b939cfc12cffe28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 12:34:13 +0400 Subject: [PATCH 091/130] Updated on 2026-08-14 --- .../impl/main/model/YieldSupplyModel.kt | 153 +++++++++--------- 1 file changed, 76 insertions(+), 77 deletions(-) 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 c6bb211f29..25e0ab888d 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 @@ -78,6 +78,7 @@ internal class YieldSupplyModel @Inject constructor( var userWallet: UserWallet by Delegates.notNull() private val fetchCurrencyJobHolder = JobHolder() + private val loadStatusJobHolder = JobHolder() private var lastStatusCheckTimestamp = 0L private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) @@ -134,22 +135,20 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun loadTokenStatus() { + private suspend fun loadTokenStatus() { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update( - YieldSupplyTokenStatusSuccessTransformer( - tokenStatus = tokenStatus, - onStartEarningClick = ::onStartEarningClick, - ), - ) - }.onLeft { - Timber.e(it) - uiState.update { YieldSupplyUM.Initial } - } - } + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update( + YieldSupplyTokenStatusSuccessTransformer( + tokenStatus = tokenStatus, + onStartEarningClick = ::onStartEarningClick, + ), + ) + }.onLeft { + Timber.e(it) + uiState.update { YieldSupplyUM.Initial } + } } override fun onStartEarningClick() { @@ -183,7 +182,9 @@ internal class YieldSupplyModel @Inject constructor( } @Suppress("MaximumLineLength") - private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch { + private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch( + dispatchers.default, + ) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus( userWallet.walletId, @@ -234,7 +235,7 @@ internal class YieldSupplyModel @Inject constructor( lastStatusCheckTimestamp = 0L } } - } + }.saveIn(loadStatusJobHolder) private fun showProcessing(status: YieldSupplyEnterStatus) { uiState.update { @@ -246,24 +247,21 @@ internal class YieldSupplyModel @Inject constructor( fetchCurrencyWithDelay() } - private fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { + private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus - modelScope - .launch { - yieldSupplyRepository.saveTokenProtocolStatus( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - yieldSupplyEnterStatus = null, - ) - if (yieldSupplyStatus?.isActive == true) { - loadActiveState( - cryptoCurrencyStatus = cryptoCurrencyStatus, - yieldSupplyStatus = yieldSupplyStatus, - ) - } else { - loadTokenStatus() - } - } + yieldSupplyRepository.saveTokenProtocolStatus( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + yieldSupplyEnterStatus = null, + ) + if (yieldSupplyStatus?.isActive == true) { + loadActiveState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yieldSupplyStatus = yieldSupplyStatus, + ) + } else { + loadTokenStatus() + } } private fun fetchCurrencyWithDelay() { @@ -280,7 +278,10 @@ internal class YieldSupplyModel @Inject constructor( }.saveIn(fetchCurrencyJobHolder) } - private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) { + private suspend fun loadActiveState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldSupplyStatus: YieldSupplyStatus, + ) { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend val isShowInfoIconPrevState = when (val state = uiState.value) { @@ -295,50 +296,48 @@ internal class YieldSupplyModel @Inject constructor( ), ) } - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = combinedReference( + resourceReference( + R.string.yield_module_token_details_earn_notification_apy, ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = combinedReference( - resourceReference( - R.string.yield_module_token_details_earn_notification_apy, - ), - stringReference(" ${tokenStatus.apy}%"), - ), - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = tokenStatus.apy.toString(), - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) - }.onLeft { t -> - Timber.e(t) - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, - ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = TextReference.EMPTY, - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = "", - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + stringReference(" ${tokenStatus.apy}%"), + ), + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = tokenStatus.apy.toString(), + ) } - } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + }.onLeft { t -> + Timber.e(t) + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = TextReference.EMPTY, + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = "", + ) + } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + } } private fun computeAndApplyShowInfoIcon(cryptoCurrencyStatus: CryptoCurrencyStatus) { From 587a31520daa8c4ba29be45d481252c9ffadf94b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 13:35:11 +0300 Subject: [PATCH 092/130] Updated on 2026-08-14 --- .../root/RootDetectedWarningComponent.kt | 68 ++++++++++++++ .../root/RootDetectedWarningContent.kt | 93 +++++++++++++++++++ .../com/tangem/tap/routing/RootContent.kt | 3 + .../component/impl/DefaultRoutingComponent.kt | 9 ++ .../local/preferences/PreferencesKeys.kt | 2 + core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 + .../core/ui/components/DialogFullScreen.kt | 70 +++++++------- .../message/MessageBottomSheetV2.kt | 24 ++--- .../ui/components/icons/HighlightedIcon.kt | 39 ++++++++ .../settings/DefaultSettingsRepository.kt | 11 +++ .../repositories/SettingsRepository.kt | 4 + 12 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt new file mode 100644 index 0000000000..16077bce72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.DialogFullScreen +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateProperty") +class RootDetectedWarningComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, + private val securityInfoProvider: DeviceSecurityInfoProvider, + private val settingsRepository: SettingsRepository, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) } + + suspend fun tryToShowWarningAndWaitContinuation() { + if (isShown.value) return + + if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) { + isShown.value = true + } + + isShown.first { it == false } // Wait until the warning is dismissed + } + + @Composable + override fun Content(modifier: Modifier) { + val isShownState by isShown.collectAsStateWithLifecycle() + + if (isShownState) { + DialogFullScreen(onDismissRequest = {}) { + RootDetectedWarningContent( + modifier = modifier, + onContinueClick = remember(this) { ::onContinueClick }, + ) + } + } + } + + private fun onContinueClick() { + componentScope.launch { + settingsRepository.setRootDetectedWarningShown(true) + isShown.value = false + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt new file mode 100644 index 0000000000..9dc01d8a13 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt @@ -0,0 +1,93 @@ +package com.tangem.tap.features.root + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.icons.HighlightedIcon +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .statusBarsPadding() + .padding(horizontal = 16.dp), + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + InfoBlock( + modifier = Modifier.padding(top = 48.dp, bottom = 24.dp), + ) + } + + PrimaryButton( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_understand_continue), + onClick = onContinueClick, + ) + } +} + +@Composable +private fun InfoBlock(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HighlightedIcon( + icon = R.drawable.ic_alert_circle_24, + iconTint = TangemTheme.colors.icon.warning, + ) + + SpacerH(20.dp) + + Text( + text = stringResourceSafe(R.string.root_detected_warning_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH(12.dp) + + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.root_detected_warning_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + RootDetectedWarningContent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index add9354694..c21f7d2617 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -47,6 +47,7 @@ internal fun RootContent( modifier: Modifier = Modifier, wcContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, + rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, ) { val context = LocalContext.current @@ -82,6 +83,8 @@ internal fun RootContent( hotAccessCodeContent(Modifier.fillMaxSize()) + rootDetectedWarningContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) 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 65ece0eaad..23bc630cec 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 @@ -38,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -64,6 +65,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val tangemHotSDKProxy: TangemHotSDKProxy, private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy, + private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory, private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -85,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor( .create(child("hotAccessCodeRequestComponent"), Unit) } + private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy { + rootDetectedWarningComponentFactory + .create(child("rootDetectedWarningComponent"), Unit) + } + private val navigation = navigationProvider.getOrCreateTyped() private val stack: Value> = childStack( @@ -134,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private fun initializeInitialNavigation() { if (initialStack.isNullOrEmpty()) { componentScope.launch { + rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation() val initialRoute = resolveInitialRoute() router.replaceAll(initialRoute) } @@ -177,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( modifier = modifier, wcContent = { wcRoutingComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, + rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index b52614222b..78e99e42bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -27,6 +27,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") } + val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d5eaa197f..03ce372d0f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -2017,7 +2017,7 @@ %1$s выведено из Aave Режим доходности инициализирован Режим доходности реактивирован - Перевод средств в Aave + Перевод в Aave %1$s отправлено в Aave Вывод из Aave Автоматически diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 524e2ec97f..07f6bd26c8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1091,6 +1091,8 @@ Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! + Devices with root access are considered less secure. Your data may be exposed to additional risks. + Root access detected Log into the app and check your balance without scanning the card or ring Access the app Allow to use biometrics diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt index 6f70db0ba1..44e48c0fd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt @@ -37,44 +37,46 @@ fun DialogFullScreen( decorFitsSystemWindows = false, ), content = { - val activityWindow = getActivityWindow() - val dialogWindow = getDialogWindow() - val parentView = LocalView.current.parent as View - SideEffect { - if (activityWindow != null && dialogWindow != null) { - val attributes = WindowManager.LayoutParams().apply { - copyFrom(activityWindow.attributes) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - } else { - flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - } - type = dialogWindow.attributes.type - } - - dialogWindow.attributes = attributes - parentView.layoutParams = - FrameLayout.LayoutParams( - activityWindow.decorView.width, - activityWindow.decorView.height, - ) - } - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - val systemUiController = rememberSystemUiController(getActivityWindow()) - val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - + ProvideSystemBarsIconsController { + val activityWindow = getActivityWindow() + val dialogWindow = getDialogWindow() + val parentView = LocalView.current.parent as View SideEffect { - systemUiController.setSystemBarsColor(color = Color.Transparent) - dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + if (activityWindow != null && dialogWindow != null) { + val attributes = WindowManager.LayoutParams().apply { + copyFrom(activityWindow.attributes) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + } else { + flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + } + type = dialogWindow.attributes.type + } + + dialogWindow.attributes = attributes + parentView.layoutParams = + FrameLayout.LayoutParams( + activityWindow.decorView.width, + activityWindow.decorView.height, + ) + } } - } - SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + val systemUiController = rememberSystemUiController(getActivityWindow()) + val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { - content() + SideEffect { + systemUiController.setSystemBarsColor(color = Color.Transparent) + dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + } + } + + SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + + Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { + content() + } } }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index f31c547290..cb3f2c538d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -2,17 +2,13 @@ package com.tangem.core.ui.components.bottomsheets.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -25,6 +21,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.icons.HighlightedIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -144,20 +141,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning } - Box( - modifier = modifier - .size(TangemTheme.dimens.size56) - .clip(CircleShape) - .background(backgroundColor.copy(alpha = 0.1F)), - contentAlignment = Alignment.Center, - content = { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size32), - painter = painterResource(icon.res), - contentDescription = null, - tint = tint, - ) - }, + HighlightedIcon( + modifier = modifier, + icon = icon.res, + iconTint = tint, + backgroundColor = backgroundColor, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt new file mode 100644 index 0000000000..a94578a5ff --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.icons + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun HighlightedIcon( + @DrawableRes icon: Int, + iconTint: Color, + modifier: Modifier = Modifier, + backgroundColor: Color = iconTint, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape) + .background(backgroundColor.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size32), + painter = painterResource(icon), + contentDescription = null, + tint = iconTint, + ) + }, + ) +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index dea79f4d3a..588d3b71e3 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -193,4 +193,15 @@ internal class DefaultSettingsRepository( default = false, ) } + + override suspend fun isRootDetectedWarningShown(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, + default = false, + ) + } + + override suspend fun setRootDetectedWarningShown(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, value = value) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 0a8f854790..68ffcaf957 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -56,4 +56,8 @@ interface SettingsRepository { suspend fun setGooglePayAvailability(value: Boolean) suspend fun isGooglePayAvailability(): Boolean + + suspend fun isRootDetectedWarningShown(): Boolean + + suspend fun setRootDetectedWarningShown(value: Boolean) } \ No newline at end of file From e0fe8be5588958432ceb29c92ad8d9b04f4b9653 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 13:09:29 +0500 Subject: [PATCH 093/130] Updated on 2026-08-14 --- .../com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt | 1 - 1 file changed, 1 deletion(-) 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 f08755fcab..13f1a7ab8f 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 @@ -149,7 +149,6 @@ internal class AccessCodeModel @Inject constructor( onClick = { uiState.update { currentState -> currentState.copy( - accessCode = "", onAccessCodeChange = ::onAccessCodeChange, requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent), ) From 71e4d3a6edeb6d2021eb7f220fbcb127e313bd09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 16:57:19 +0500 Subject: [PATCH 094/130] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 4 ---- features/hot-wallet/impl/build.gradle.kts | 1 + .../addexistingwallet/entry/AddExistingWalletModel.kt | 9 ++++----- .../walletactivation/entry/WalletActivationModel.kt | 9 ++++----- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 076a21b682..690ab43218 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,7 +40,6 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase -import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -130,9 +129,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var cardRepository: CardRepository - @Inject - lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase - @Inject lateinit var backupServiceHolder: BackupServiceHolder diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 380b51678e..36d956f24c 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.notifications) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index 51cfc12211..ed1d37d3f9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -15,8 +15,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -25,7 +25,6 @@ import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow @@ -37,7 +36,7 @@ import javax.inject.Inject internal class AddExistingWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val notificationsRepository: NotificationsRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @@ -79,8 +78,8 @@ internal class AddExistingWalletModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { + val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() + if (shouldAskNotificationPermissions) { stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) } else { stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 283d1c7eea..129947fdb7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent @@ -28,7 +28,6 @@ import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartCompone import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks @@ -44,7 +43,7 @@ internal class WalletActivationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val notificationsRepository: NotificationsRepository, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, @@ -118,8 +117,8 @@ internal class WalletActivationModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { + val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() + if (shouldAskNotificationPermissions) { stackNavigation.replaceAll(WalletActivationRoute.PushNotifications) } else { stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) From 0594e1b680642e643aa1046fe8560520c7c8089c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 17:25:04 +0500 Subject: [PATCH 095/130] Updated on 2026-08-14 --- .../sdk/impl/DefaultTangemSdkManager.kt | 33 ++++- .../domain/sdk/impl/MockTangemSdkManager.kt | 14 +- ...mPayGenerateAddressAndSignChallengeTask.kt | 15 +- .../visa/TangemPaySignWithdrawalHashTask.kt | 15 +- .../visa/VisaCustomerWalletApproveTask.kt | 80 ++--------- data/visa/build.gradle.kts | 2 + .../DefaultTangemPayAuthDataSource.kt | 34 ++--- .../pay/datasource/TangemPayHotSdkManager.kt | 130 ++++++++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 3 +- .../repository/DefaultOnboardingRepository.kt | 15 +- .../DefaultTangemPaySwapRepository.kt | 36 ++--- .../DefaultTangemPayWithdrawUseCase.kt | 6 +- .../visa/DefaultTangemPayRemoteDataSource.kt | 3 +- .../ethereum/WcEthMessageSignUseCase.kt | 1 + .../domain/card/common/visa/VisaUtilities.kt | 35 ++++- .../domain/pay}/WithdrawalSignatureResult.kt | 2 +- .../pay/datasource/TangemPayAuthDataSource.kt | 10 +- .../pay/repository/TangemPaySwapRepository.kt | 4 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 23 ++-- .../tangempay/TangemPayWithdrawUseCase.kt | 4 +- .../tangem/feature/swap/model/SwapModel.kt | 2 +- .../model/TangemPayOnboardingModel.kt | 33 ++--- .../com/tangem/sdk/api/TangemSdkManager.kt | 6 +- 23 files changed, 298 insertions(+), 208 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt rename domain/visa/{src/main/kotlin/com/tangem/domain/pay/model => models/src/main/kotlin/com/tangem/domain/pay}/WithdrawalSignatureResult.kt (83%) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index a303cd8df9..981b99ecf8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -3,6 +3,9 @@ package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.Log import com.tangem.Message import com.tangem.TangemSdk @@ -24,6 +27,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.* import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles @@ -516,23 +520,44 @@ internal class DefaultTangemSdkManager( override suspend fun tangemPayProduceInitialCredentials( cardId: String, - ): CompletionResult { + ): Either { return coroutineScope { - runTaskAsyncReturnOnMain( + val result = runTaskAsyncReturnOnMain( runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), cardId = cardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) + + return@coroutineScope when (result) { + is CompletionResult.Failure<*> -> result.error.left() + is CompletionResult.Success -> result.data.right() + } } } - override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + override suspend fun getWithdrawalSignature( + cardId: String, + hash: String, + ): Either { return coroutineScope { - runTaskAsyncReturnOnMain( + val result = runTaskAsyncReturnOnMain( runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()), cardId = cardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) + + return@coroutineScope when (result) { + is CompletionResult.Failure<*> -> { + if (result.error is TangemSdkError.UserCancelled) { + WithdrawalSignatureResult.Cancelled.right() + } else { + result.error.left() + } + } + is CompletionResult.Success -> { + WithdrawalSignatureResult.Success(result.data).right() + } + } } } // endregion diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 91fe1414d6..54e9e33aca 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -18,7 +19,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse @@ -215,11 +220,14 @@ class MockTangemSdkManager( override suspend fun tangemPayProduceInitialCredentials( cardId: String, - ): CompletionResult { + ): Either { error("Not implemented") } - override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + override suspend fun getWithdrawalSignature( + cardId: String, + hash: String, + ): Either { error("Not implemented") } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt index bc05e53d63..4550d3dc52 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -3,7 +3,6 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.card.CardWallet -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback @@ -42,12 +41,14 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( private suspend fun runSuspend(session: CardSession): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) val address = when (val derivationResult = runDerivationTask(session, wallet)) { is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error) - is CompletionResult.Success -> generateAddressFromExtendedKey(derivationResult.data) + is CompletionResult.Success -> VisaUtilities.generateAddressFromExtendedKey( + extendedPublicKey = derivationResult.data, + ) } val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey) @@ -119,14 +120,6 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( return deferred.await() } - private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { - val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey( - extendedPublicKey = extendedPublicKey, - cachedIndex = null, - ) - return derivationData.address - } - @AssistedFactory interface Factory { fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt index d5bc3b64c1..2cf1b0f42c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt @@ -1,15 +1,11 @@ package com.tangem.tap.domain.tasks.visa -import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.toDecompressedPublicKey -import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -40,7 +36,7 @@ class TangemPaySignWithdrawalHashTask( private fun proceedSign(card: Card, session: CardSession, callback: CompletionCallback) { val derivationPath = VisaUtilities.customDerivationPath - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) return } @@ -71,7 +67,7 @@ class TangemPaySignWithdrawalHashTask( private fun signData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, - extendedPublicKey: ExtendedPublicKey?, + extendedPublicKey: ExtendedPublicKey, session: CardSession, callback: CompletionCallback, ) { @@ -84,12 +80,11 @@ class TangemPaySignWithdrawalHashTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + val rsvSignature = VisaUtilities.unmarshallSignature( signature = result.data.signature, hash = hash, - publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() - ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().lowercase() + extendedPublicKey = extendedPublicKey, + ) callback(CompletionResult.Success(rsvSignature)) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 3439d72093..d4c6dc4800 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,28 +1,19 @@ package com.tangem.tap.domain.tasks.visa -import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak -import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.CardWallet -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.toDecompressedPublicKey -import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility -import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet -import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand @@ -46,11 +37,7 @@ class VisaCustomerWalletApproveTask( return } - if (card.settings.isHDWalletAllowed) { - proceedApprove(card, session, callback) - } else { - proceedApproveWithLegacyCard(card, session, callback) - } + proceedApprove(card, session, callback) } private fun proceedApprove( @@ -60,7 +47,7 @@ class VisaCustomerWalletApproveTask( ) { val derivationPath = VisaUtilities.customDerivationPath - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) return } @@ -114,43 +101,15 @@ class VisaCustomerWalletApproveTask( ) } - private fun proceedApproveWithLegacyCard( - card: Card, - session: CardSession, - callback: CompletionCallback, - ) { - val publicKey = findKeyWithoutDerivation( - targetAddress = visaDataForApprove.targetAddress, - card = CardDTO(card), - ).getOrElse { error -> - callback(CompletionResult.Failure(error.tangemError)) - return - } - - signApproveData( - targetWalletPublicKey = publicKey, - derivationPath = null, - extendedPublicKey = null, - session = session, - callback = callback, - ) - } - - // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK - private fun hashPersonalMessage(message: ByteArray): ByteArray { - val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() - return (prefix + message).toKeccak() - } - private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, - extendedPublicKey: ExtendedPublicKey?, + extendedPublicKey: ExtendedPublicKey, session: CardSession, callback: CompletionCallback, ) { - val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}" - val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) + val content = VisaUtilities.signWithNonceMessage(visaDataForApprove.hashToSign) + val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) val signTask = SignHashCommand( hash = hash, @@ -161,36 +120,13 @@ class VisaCustomerWalletApproveTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + val rsvSignature = VisaUtilities.unmarshallSignature( signature = result.data.signature, hash = hash, - publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() - ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().lowercase() - - scanCard( - session = session, - callback = callback, - signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress), + extendedPublicKey = extendedPublicKey, ) - } - is CompletionResult.Failure -> { - callback(CompletionResult.Failure(result.error)) - } - } - } - } - private fun scanCard( - signedData: VisaSignedDataByCustomerWallet, - session: CardSession, - callback: CompletionCallback, - ) { - val scanTask = ScanTask() - scanTask.run(session) { result -> - when (result) { - is CompletionResult.Success -> { - callback(CompletionResult.Success(signedData)) + visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 1470c2b5d9..f893a530d6 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /** Project - Data */ implementation(projects.core.datasource) implementation(projects.core.error) + implementation(projects.core.error.ext) implementation(projects.core.security) implementation(projects.data.common) @@ -60,6 +61,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) implementation(projects.libs.tangemSdkApi) /** DI */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 9eea6d7429..6af402c591 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -1,42 +1,34 @@ package com.tangem.data.pay.datasource import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.sdk.api.TangemSdkManager import javax.inject.Inject internal class DefaultTangemPayAuthDataSource @Inject constructor( private val tangemSdkManager: TangemSdkManager, + private val tangemPayHotSdkManager: TangemPayHotSdkManager, ) : TangemPayAuthDataSource { - override suspend fun produceInitialCredentials(cardId: String): Either { - return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) { - is CompletionResult.Failure<*> -> initialCredentials.error.left() - is CompletionResult.Success -> initialCredentials.data.right() + override suspend fun produceInitialCredentials( + userWallet: UserWallet, + ): Either { + return when (userWallet) { + is UserWallet.Cold -> tangemSdkManager.tangemPayProduceInitialCredentials(cardId = userWallet.cardId) + is UserWallet.Hot -> tangemPayHotSdkManager.produceInitialCredentials(userWallet) } } override suspend fun getWithdrawalSignature( - cardId: String, + userWallet: UserWallet, hash: String, ): Either { - return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) { - is CompletionResult.Failure<*> -> { - if (signResult.error is TangemSdkError.UserCancelled) { - WithdrawalSignatureResult.Cancelled.right() - } else { - signResult.error.left() - } - } - is CompletionResult.Success -> { - WithdrawalSignatureResult.Success(signResult.data).right() - } + return when (userWallet) { + is UserWallet.Cold -> tangemSdkManager.getWithdrawalSignature(cardId = userWallet.cardId, hash = hash) + is UserWallet.Hot -> tangemPayHotSdkManager.getWithdrawalSignature(hotWallet = userWallet, hash = hash) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt new file mode 100644 index 0000000000..6c2eac2122 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt @@ -0,0 +1,130 @@ +package com.tangem.data.pay.datasource + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.either +import com.tangem.common.extensions.hexToBytes +import com.tangem.core.error.ext.tangemError +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.error.VisaCardScanError +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.DataToSign +import com.tangem.hot.sdk.model.DeriveWalletRequest +import com.tangem.hot.sdk.model.UnlockHotWallet +import javax.inject.Inject + +internal class TangemPayHotSdkManager @Inject constructor( + private val hotWalletAccessor: HotWalletAccessor, + private val tangemHotSdk: TangemHotSdk, + private val tangemPayAuthRemoteDataSource: TangemPayRemoteDataSource, +) { + + suspend fun produceInitialCredentials(hotWallet: UserWallet.Hot): Either = + withUnlockedHotWallet(hotWallet) { unlockHotWallet -> + val extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet) + val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey) + val challenge = tangemPayAuthRemoteDataSource.getCustomerWalletAuthChallenge( + customerWalletAddress = address, + customerWalletId = hotWallet.walletId.stringValue, + ).getOrElse { raise(it.tangemError) } + + val content = VisaUtilities.signWithNonceMessage(challenge.challenge) + val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) + val signature = getSignature( + unlockHotWallet = unlockHotWallet, + hash = hash, + extendedPublicKey = extendedPublicKey, + ) + + val authTokens = tangemPayAuthRemoteDataSource.getTokenWithCustomerWallet( + sessionId = challenge.session.sessionId, + signature = signature, + nonce = challenge.challenge, + ).getOrElse { raise(VisaActivationError.FailedRemoteState.tangemError) } + + TangemPayInitialCredentials( + customerWalletAddress = address, + authTokens = authTokens, + ) + } + + suspend fun getWithdrawalSignature( + hotWallet: UserWallet.Hot, + hash: String, + ): Either = withUnlockedHotWallet(hotWallet) { unlockHotWallet -> + val signature = getSignature( + unlockHotWallet = unlockHotWallet, + hash = hash.hexToBytes(), + extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet), + ) + + WithdrawalSignatureResult.Success(signature) + } + + private suspend fun Raise.getExtendedPublicKey(unlockHotWallet: UnlockHotWallet): ExtendedPublicKey { + val publicKeyResponse = tangemHotSdk.derivePublicKey( + unlockHotWallet = unlockHotWallet, + request = DeriveWalletRequest( + requests = listOf( + DeriveWalletRequest.Request( + curve = VisaUtilities.curve, + paths = listOf(VisaUtilities.customDerivationPath), + ), + ), + ), + ) + return publicKeyResponse.responses + .firstOrNull { it.curve == VisaUtilities.curve } + ?.publicKeys[VisaUtilities.customDerivationPath] + ?: raise(VisaActivationError.MissingWallet.tangemError) + } + + private suspend fun Raise.getSignature( + unlockHotWallet: UnlockHotWallet, + hash: ByteArray, + extendedPublicKey: ExtendedPublicKey, + ): String { + val signedHashes = tangemHotSdk.signHashes( + unlockHotWallet = unlockHotWallet, + dataToSign = listOf( + DataToSign( + curve = VisaUtilities.curve, + derivationPath = VisaUtilities.customDerivationPath, + hashes = listOf(hash), + ), + ), + ) + val signature = signedHashes + .firstOrNull { it.curve == VisaUtilities.curve } + ?.signatures + ?.firstOrNull() + ?: raise(VisaCardScanError.FailedToSignChallenge.tangemError) + + return VisaUtilities.unmarshallSignature( + signature = signature, + hash = hash, + extendedPublicKey = extendedPublicKey, + ) + } + + private suspend inline fun withUnlockedHotWallet( + hotWallet: UserWallet.Hot, + block: Raise.(UnlockHotWallet) -> T, + ): Either = either { + try { + val unlockHotWallet = hotWalletAccessor.getContextualUnlock(hotWallet.hotWalletId) + ?: hotWalletAccessor.unlockContextual(hotWallet.hotWalletId) + block(unlockHotWallet) + } finally { + hotWalletAccessor.clearContextualUnlock(hotWallet.hotWalletId) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index ffcd79fb85..719542728a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -80,9 +80,8 @@ internal interface TangemPayDataModule { deviceSecurity: DeviceSecurityInfoProvider, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( - repository = repository, + onboardingRepository = repository, customerOrderRepository = customerOrderRepository, - tangemPayOnboardingRepository = tangemPayOnboardingRepository, eligibilityManager = eligibilityManager, deviceSecurity = deviceSecurity, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index c3ab4f66c0..348ae72b3b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -65,15 +65,16 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun produceInitialData(userWalletId: UserWalletId) { withContext(dispatcherProvider.io) { - val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId)) + val userWallet = getUserWallet(userWalletId) + val initialCredentials = authDataSource.produceInitialCredentials(userWallet) .fold( ifLeft = { error -> error("Can not produce initial data: ${error.message}") }, ifRight = { it }, ) // should storeCheckCustomerWalletResult because we already know this - tangemPayStorage.storeCheckCustomerWalletResult(userWalletId, true) + tangemPayStorage.storeCheckCustomerWalletResult(userWallet.walletId, true) tangemPayStorage.storeCustomerWalletAddress( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, customerWalletAddress = initialCredentials.customerWalletAddress, ) tangemPayStorage.storeAuthTokens( @@ -120,17 +121,13 @@ internal class DefaultOnboardingRepository @Inject constructor( } } - private fun getCardId(userWalletId: UserWalletId): String { + private fun getUserWallet(userWalletId: UserWalletId): UserWallet { val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } } else { userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } } ?: error("no userWallet found") - return if (userWallet is UserWallet.Cold) { - userWallet.cardId - } else { - TODO("[REDACTED_JIRA]") - } + return userWallet } private suspend fun getCustomerInfo( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt index 042686b6e7..6bc1774f28 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt @@ -1,23 +1,20 @@ package com.tangem.data.pay.repository import arrow.core.Either +import arrow.core.left import com.tangem.core.error.UniversalError import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest import com.tangem.datasource.api.pay.models.request.WithdrawRequest import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.extensions.addHexPrefix import java.math.BigDecimal import java.math.RoundingMode @@ -30,29 +27,25 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, private val authDataSource: TangemPayAuthDataSource, - private val userWalletsListManager: UserWalletsListManager, - private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val quotesFetcher: QuotesFetcher, private val tangemPayStorage: TangemPayStorage, ) : TangemPaySwapRepository { override suspend fun withdraw( - userWalletId: UserWalletId, + userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, ): Either { val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) - return requestHelper.performRequest(userWalletId) { authHeader -> + return requestHelper.performRequest(userWallet.walletId) { authHeader -> val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) }.map { data -> - val result = data.result - if (result == null) return Either.Left(VisaApiError.WithdrawalDataError) + val result = data.result ?: return VisaApiError.WithdrawalDataError.left() val signatureResult = authDataSource.getWithdrawalSignature( - cardId = getCardId(userWalletId), + userWallet = userWallet, hash = result.hash, ).getOrNull() @@ -61,7 +54,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( Either.Right(WithdrawalResult.Cancelled) } is WithdrawalSignatureResult.Success -> { - requestHelper.performRequest(userWalletId) { authHeader -> + requestHelper.performRequest(userWallet.walletId) { authHeader -> val request = WithdrawRequest( amountInCents = amountInCents, recipientAddress = receiverAddress, @@ -74,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( .mapLeft { return Either.Left(VisaApiError.WithdrawError) } .map { response -> val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId) + if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId) WithdrawalResult.Success } } @@ -103,17 +96,4 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( return quotes?.quotes[cryptoCurrencyId.value]?.price } - - private fun getCardId(userWalletId: UserWalletId): String { - val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } - } else { - userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } - } ?: error("No User Wallet found") - return if (userWallet is UserWallet.Cold) { - userWallet.cardId - } else { - TODO("[REDACTED_JIRA]") - } - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index 3ccde5d629..36b85985ad 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.data.pay.usecase import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase @@ -15,13 +15,13 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( ) : TangemPayWithdrawUseCase { override suspend fun invoke( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, ): Either { return repository.withdraw( - userWalletId = userWalletId, + userWallet = userWallet, cryptoAmount = cryptoAmount, receiverAddress = receiverCexAddress, cryptoCurrencyId = cryptoCurrencyId, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt index c2a927964b..63d2853352 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalle import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayAuthTokens @@ -56,7 +57,7 @@ internal class DefaultTangemPayRemoteDataSource @Inject constructor( authType = "customer_wallet", sessionId = sessionId, signature = signature, - messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + messageFormat = VisaUtilities.signWithNonceMessage(nonce), ), ).getOrThrow() }.map { response -> diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index 36d3087190..033ffd4461 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -83,6 +83,7 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor( object LegacySdkHelper { private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK fun prepareToSendMessageData(signedHash: ByteArray, hashToSign: ByteArray, walletManager: WalletManager): String = UnmarshalHelper.unmarshalSignatureExtended( signature = signedHash, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt index fe41ac4f05..2ed1be037d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt @@ -1,9 +1,15 @@ package com.tangem.domain.card.common.visa +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion +import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO private const val VISA_BATCH_START = "AE" @@ -11,15 +17,16 @@ private const val VISA_BATCH_START_2 = "FFFC" object VisaUtilities { - const val tokenId = "tether" - val visaBlockchain = Blockchain.Polygon val visaDefaultDerivationPath get() = visaBlockchain.derivationPath(DerivationStyle.V3) val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0") + val curve = EllipticCurve.Secp256k1 - fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) + fun signWithNonceMessage(nonce: String): String { + return "Tangem Pay wants to sign in with your account. Nonce: $nonce" + } fun isVisaCard(card: CardDTO): Boolean { return isVisaCard(card.firmwareVersion.doubleValue, card.batchId) @@ -29,4 +36,26 @@ object VisaUtilities { return firmwareVersion in FirmwareVersion.visaRange && (batchId.startsWith(VISA_BATCH_START) || batchId.startsWith(VISA_BATCH_START_2)) } + + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK + fun hashPersonalMessage(message: ByteArray): ByteArray { + val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() + return (prefix + message).toKeccak() + } + + fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { + val derivationData = visaBlockchain.makeAddressesFromExtendedPublicKey( + extendedPublicKey = extendedPublicKey, + cachedIndex = null, + ) + return derivationData.address + } + + fun unmarshallSignature(signature: ByteArray, hash: ByteArray, extendedPublicKey: ExtendedPublicKey): String { + return UnmarshalHelper.unmarshalSignatureExtended( + signature = signature, + hash = hash, + publicKey = extendedPublicKey.publicKey.toDecompressedPublicKey(), + ).asRSVLegacyEVM().toHexString().lowercase() + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt similarity index 83% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt rename to domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt index 42f55abd15..18e996d13d 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.pay.model +package com.tangem.domain.pay sealed class WithdrawalSignatureResult { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 66a037381f..4ab30e9ff7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -1,12 +1,16 @@ package com.tangem.domain.pay.datasource import arrow.core.Either -import com.tangem.domain.pay.model.WithdrawalSignatureResult +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials interface TangemPayAuthDataSource { - suspend fun produceInitialCredentials(cardId: String): Either + suspend fun produceInitialCredentials(userWallet: UserWallet): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): Either + suspend fun getWithdrawalSignature( + userWallet: UserWallet, + hash: String, + ): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt index d27ac30af7..fed66aab7d 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt @@ -3,14 +3,14 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPaySwapRepository { suspend fun withdraw( - userWalletId: UserWalletId, + userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 10a21fbb78..9636a5136b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -21,9 +21,8 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" * Works only if the user already authorised at least once (won't emit anything otherwise) */ class TangemPayMainScreenCustomerInfoUseCase( - private val repository: OnboardingRepository, + private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, - private val tangemPayOnboardingRepository: OnboardingRepository, private val eligibilityManager: TangemPayEligibilityManager, private val deviceSecurity: DeviceSecurityInfoProvider, ) { @@ -32,7 +31,7 @@ class TangemPayMainScreenCustomerInfoUseCase( field = MutableStateFlow(value = mapOf()) suspend fun fetch(userWalletId: UserWalletId) { - Timber.tag(TAG).i("fetch: $userWalletId") + Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") @@ -43,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return // fast exit } - repository.checkCustomerWallet(userWalletId) + onboardingRepository.checkCustomerWallet(userWalletId) .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") @@ -64,7 +63,7 @@ class TangemPayMainScreenCustomerInfoUseCase( // if there's no tangem pay, check eligibility and show onboarding banner val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId } if (isEligible) { - if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) { + if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) { updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) } else { updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) @@ -95,10 +94,10 @@ class TangemPayMainScreenCustomerInfoUseCase( private suspend fun proceedWithPaeraCustomerResult( userWalletId: UserWalletId, ): Either { - if (!tangemPayOnboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return TangemPayCustomerInfoError.RefreshNeededError.left() } - val orderId = repository.getOrderId(userWalletId) + val orderId = onboardingRepository.getOrderId(userWalletId) return if (orderId != null) { proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) } else { @@ -109,7 +108,7 @@ class TangemPayMainScreenCustomerInfoUseCase( private suspend fun proceedWithoutOrder( userWalletId: UserWalletId, ): Either { - return repository.getCustomerInfo(userWalletId) + return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> Timber.tag(TAG).e("mapErrorForCustomer: $error") error.mapErrorForCustomer() @@ -118,7 +117,7 @@ class TangemPayMainScreenCustomerInfoUseCase( Timber.tag(TAG).i("customerInfo") if (customerInfo.cardInfo == null && customerInfo.isKycApproved) { // If order id wasn't saved -> start order creation and get customer info - repository.createOrder(userWalletId) + onboardingRepository.createOrder(userWalletId) } MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN) } @@ -148,10 +147,10 @@ class TangemPayMainScreenCustomerInfoUseCase( OrderStatus.CANCELED, OrderStatus.UNKNOWN, -> { - repository.clearOrderId(userWalletId) + onboardingRepository.clearOrderId(userWalletId) // If order was cancelled -> start order creation - if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId) - repository.getCustomerInfo(userWalletId = userWalletId) + if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) .mapLeft { it.mapErrorForCustomer() } .map { customerInfo -> MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index 910f50090c..e2abfd227f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -3,14 +3,14 @@ package com.tangem.domain.tangempay import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPayWithdrawUseCase { suspend operator fun invoke( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index f4b0a59776..cbed111a44 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -845,7 +845,7 @@ internal class SwapModel @Inject constructor( private suspend fun processTangemPayWithdrawal(swapTransactionState: SwapTransactionState.TangemPayWithdrawalData) { tangemPayWithdrawUseCase( - userWalletId = userWalletId, + userWallet = userWallet, cryptoAmount = swapTransactionState.cryptoAmount, cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, receiverCexAddress = swapTransactionState.cexAddress, 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 bc39e6d47b..764ff69042 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 @@ -89,9 +89,7 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun checkCustomerInfo(userWalletId: UserWalletId) { modelScope.launch { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) - repository.getCustomerInfo( - userWalletId = userWalletId, - ) + repository.getCustomerInfo(userWalletId = userWalletId) .onRight { customerInfo -> uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) when { @@ -159,21 +157,20 @@ internal class TangemPayOnboardingModel @Inject constructor( uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } - repository.getCustomerInfo( - userWalletId = userWalletId, - ).fold( - ifLeft = { error -> - Timber.e("Error getCustomerInfo: ${error.errorCode}") - uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) - }, - ifRight = { customerInfo -> - if (customerInfo.isKycApproved) { - back() - } else { - openKyc(userWalletId) - } - }, - ) + repository.getCustomerInfo(userWalletId = userWalletId) + .fold( + ifLeft = { error -> + Timber.e("Error getCustomerInfo: ${error.errorCode}") + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) + }, + ifRight = { customerInfo -> + if (customerInfo.isKycApproved) { + back() + } else { + openKyc(userWalletId) + } + }, + ) } } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 628ecd897d..07a2d02773 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -2,6 +2,7 @@ package com.tangem.sdk.api import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -16,6 +17,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaDataForApprove @@ -159,8 +161,8 @@ interface TangemSdkManager { visaDataForApprove: VisaDataForApprove, ): CompletionResult - suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult + suspend fun tangemPayProduceInitialCredentials(cardId: String): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult + suspend fun getWithdrawalSignature(cardId: String, hash: String): Either // endregion } \ No newline at end of file From a8faaffcab816cce7bc63c7d4bbde5fff100bd7b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 18:28:10 +0400 Subject: [PATCH 096/130] Updated on 2026-08-14 --- .../models/response/CardDetailsResponse.kt | 1 + .../pay/models/response/CustomerMeResponse.kt | 1 + core/res/src/main/res/values/strings.xml | 4 + .../repository/DefaultOnboardingRepository.kt | 1 + .../DefaultTangemPayCardDetailsRepository.kt | 68 ++++-- .../tangem/data/pay/util/RainCryptoUtil.kt | 20 ++ .../domain/pay/TangemPayDetailsConfig.kt | 1 + .../tangem/domain/pay/model/CustomerInfo.kt | 1 + .../TangemPayCardDetailsRepository.kt | 2 + .../components/TangemPayDetailsComponent.kt | 8 + .../components/TangemPayViewPinComponent.kt | 44 ++++ .../tangempay/di/TangemPayModelModule.kt | 6 + .../entity/TangemPayDetailsNavigation.kt | 6 + .../entity/TangemPayDetailsStateFactory.kt | 4 +- .../tangempay/entity/TangemPayViewPinUM.kt | 23 ++ .../tangempay/model/TangemPayDetailsModel.kt | 25 ++- .../tangempay/model/TangemPayViewPinModel.kt | 59 +++++ .../TangemPayViewPinErrorStateTransformer.kt | 35 +++ ...TangemPayViewPinSuccessStateTransformer.kt | 18 ++ .../features/tangempay/ui/PinDigitBox.kt | 49 +++++ .../tangempay/ui/TangemPayChangePinScreen.kt | 39 ---- .../tangempay/ui/TangemPayViewPinContent.kt | 204 ++++++++++++++++++ .../tangempay/utils/TangemPayDetailIntents.kt | 2 +- .../TangemPayUpdateInfoStateTransformer.kt | 1 + 24 files changed, 561 insertions(+), 61 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt index 978baa11f9..007bcf7428 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt @@ -18,6 +18,7 @@ data class CardDetailsResponse( @Json(name = "card_number_end") val cardNumberEnd: String, @Json(name = "pan") val pan: Secret, @Json(name = "cvv") val cvv: Secret, + @Json(name = "pin") val pin: Secret?, ) @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index baf6d5d265..cc0edd82a4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -97,5 +97,6 @@ data class CustomerMeResponse( @Json(name = "card_type") val cardType: String, @Json(name = "card_status") val cardStatus: String, @Json(name = "card_number_end") val cardNumberEnd: String, + @Json(name = "is_pin_set") val isPinSet: Boolean?, ) } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 07f6bd26c8..7e87d0d441 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1083,6 +1083,7 @@ Reset the card I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards of the current wallet + I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code. Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet. All Tangem devices have been reset. @@ -1470,6 +1471,7 @@ All set! Your card is ready to use. Add card to Google Pay Add card to Apple Pay + PIN code Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now @@ -1478,6 +1480,8 @@ Swap any asset in your portfolio for card Card details Unfreeze Card + Come back to the app if you forget it. + Your PIN code Withdraw Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 348ae72b3b..e11aa1c09b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -144,6 +144,7 @@ internal class DefaultOnboardingRepository @Inject constructor( currencyCode = fiatBalance.currency, customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, + isPinSet = response.card?.isPinSet == true, ) } else { null 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 6bb55b562c..a05f92ee7a 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 @@ -82,12 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( block = { val publicKeyBase64 = getPublicKeyBase64() val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) - val result = requestHelper.performRequest(userWalletId = userWalletId) { authHeader -> - tangemPayApi.revealCardDetails( - authHeader = authHeader, - body = CardDetailsRequest(sessionId = sessionId), - ) - }.getOrNull()?.result ?: error("Cannot reveal card details") + val result = requireNotNull( + requestHelper.performRequest(userWalletId = userWalletId) { authHeader -> + tangemPayApi.revealCardDetails( + authHeader = authHeader, + body = CardDetailsRequest(sessionId = sessionId), + ) + }.getOrNull()?.result, + ) val pan = rainCryptoUtil.decryptSecret( base64Secret = result.pan.secret, @@ -113,6 +115,38 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( ) } + override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either { + return catch( + block = { + val publicKeyBase64 = getPublicKeyBase64() + val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) + val result = requireNotNull( + requestHelper.performRequest(userWalletId = userWalletId) { authHeader -> + tangemPayApi.revealCardDetails( + authHeader = authHeader, + body = CardDetailsRequest(sessionId = sessionId), + ) + }.getOrNull()?.result, + ) + + val encryptedPin = result.pin + val pin = if (encryptedPin != null) { + rainCryptoUtil.decryptPin( + base64Secret = encryptedPin.secret, + base64Iv = encryptedPin.iv, + secretKeyBytes = secretKeyBytes, + ).takeIf { !it.isNullOrEmpty() } + } else { + null + } + secretKeyBytes.fill(0) + + pin.right() + }, + catch = ::catchException, + ) + } + override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either { return requestHelper.runWithErrorLogs(TAG) { val publicKeyBase64 = getPublicKeyBase64() @@ -120,16 +154,18 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes) secretKeyBytes.fill(0) - val status = requestHelper.request(userWalletId) { authHeader -> - tangemPayApi.setPin( - authHeader = authHeader, - body = SetPinRequest( - sessionId = sessionId, - pin = encryptedData.encryptedBase64, - iv = encryptedData.ivBase64, - ), - ) - }.result?.result ?: error("Cannot set pin code") + val status = requireNotNull( + requestHelper.request(userWalletId) { authHeader -> + tangemPayApi.setPin( + authHeader = authHeader, + body = SetPinRequest( + sessionId = sessionId, + pin = encryptedData.encryptedBase64, + iv = encryptedData.ivBase64, + ), + ) + }.result?.result, + ) when (status) { SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt index 4fa2681263..ce23bc2504 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt @@ -37,6 +37,15 @@ internal class RainCryptoUtil @Inject constructor( secretKeyBytes to sessionId } + suspend fun decryptPin(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): String? { + val pinBlock = decryptSecret( + base64Secret = base64Secret, + base64Iv = base64Iv, + secretKeyBytes = secretKeyBytes, + ) + return extractPinFromPinBlock(pinBlock) + } + suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) { val bytes = pinBlockByteArray(pin) try { @@ -113,6 +122,17 @@ internal class RainCryptoUtil @Inject constructor( return hex.toByteArray(StandardCharsets.UTF_8) } + private suspend fun extractPinFromPinBlock(pinBlock: String): String? = withContext(dispatchers.default) { + val pinLength = pinBlock[1].digitToIntOrNull() + require(pinLength == PIN_LENGTH) { "Unexpected PIN length: ${pinBlock[1]}" } + + val pinStartIndex = 2 + val pinEndIndex = pinStartIndex + pinLength + val pin = pinBlock.substring(pinStartIndex, pinEndIndex) + + pin.takeIf { value -> value.all { it.isDigit() } && value.length == PIN_LENGTH } + } + private fun ByteArray.clear() { for (i in indices) this[i] = 0 } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index da3de9f475..88f7b5556c 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class TangemPayDetailsConfig( val cardId: String, + val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, val customerWalletAddress: String, val cardNumberEnd: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index d747e41066..234366b079 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -32,5 +32,6 @@ data class CustomerInfo( val currencyCode: String, val customerWalletAddress: String, val depositAddress: String?, + val isPinSet: Boolean, ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 73a4b8e035..088b34d193 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -15,6 +15,8 @@ interface TangemPayCardDetailsRepository { suspend fun revealCardDetails(userWalletId: UserWalletId): Either + suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either + suspend fun setPin(userWalletId: UserWalletId, pin: String): Either suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 44a23b3ed8..fb099938ee 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -100,6 +100,14 @@ internal class TangemPayDetailsComponent( listener = model, ), ) + is TangemPayDetailsNavigation.ViewPinCode -> TangemPayViewPinComponent( + appComponentContext = context, + params = TangemPayViewPinComponent.Params( + walletId = navigation.userWalletId, + cardId = navigation.cardId, + listener = model, + ), + ) } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt new file mode 100644 index 0000000000..f12eb316e9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.tangempay.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.model.TangemPayViewPinModel +import com.tangem.features.tangempay.ui.TangemPayViewPinContent + +internal class TangemPayViewPinComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayViewPinModel = getOrCreateModel(params = params) + + override fun dismiss() { + model.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + BackHandler(onBack = ::dismiss) + DisableScreenshotsDisposableEffect() + TangemPayViewPinContent(state = state) + } + + data class Params( + val listener: ViewPinListener, + val walletId: UserWalletId, + val cardId: String, + ) +} + +internal interface ViewPinListener { + fun onClickChangePin() + fun onDismissViewPin() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 2d4297613c..2150e54279 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -9,6 +9,7 @@ import com.tangem.features.tangempay.model.TangemPayChangePinModel import com.tangem.features.tangempay.model.TangemPayDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryModel +import com.tangem.features.tangempay.model.TangemPayViewPinModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -53,4 +54,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayAddFundsModel::class) fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayViewPinModel::class) + fun bindTangemPayViewPinModel(model: TangemPayViewPinModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt index 86009f09f0..5f10a9ed77 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -26,4 +26,10 @@ internal sealed class TangemPayDetailsNavigation { val transaction: TangemPayTxHistoryItem, val isBalanceHidden: Boolean, ) : TangemPayDetailsNavigation() + + @Serializable + data class ViewPinCode( + val userWalletId: UserWalletId, + val cardId: String, + ) : TangemPayDetailsNavigation() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 9853165dad..55b429059a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -51,9 +51,9 @@ internal class TangemPayDetailsStateFactory( TangemPayDetailsTopBarMenuItem( type = TangemPayDetailsTopBarMenuItemType.ChangePin, dropdownItem = TangemDropdownMenuItem( - title = resourceReference(R.string.tangempay_card_details_change_pin), + title = resourceReference(R.string.tangempay_card_details_pin_code), textColor = themedColor { TangemTheme.colors.text.primary1 }, - onClick = intents::onClickChangePin, + onClick = intents::onClickPinCode, ), ), TangemPayDetailsTopBarMenuItem( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt new file mode 100644 index 0000000000..8ee03d3db4 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 + +internal sealed class TangemPayViewPinUM { + + open val onDismiss: () -> Unit = {} + + data class Loading( + override val onDismiss: () -> Unit, + ) : TangemPayViewPinUM() + + data class Content( + val pin: String, + val onClickChangePin: () -> Unit, + override val onDismiss: () -> Unit, + ) : TangemPayViewPinUM() + + data class Error( + val errorMessage: MessageBottomSheetUMV2, + override val onDismiss: () -> Unit, + ) : TangemPayViewPinUM() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index df186cb3f8..2c2b69be13 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -33,6 +33,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent +import com.tangem.features.tangempay.components.ViewPinListener import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation @@ -77,7 +78,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val orderRepository: CustomerOrderRepository, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, -) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { +) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener, ViewPinListener { private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() @@ -124,8 +125,17 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } - override fun onClickChangePin() { - router.push(TangemPayDetailsInnerRoute.ChangePIN) + override fun onClickPinCode() { + if (!params.config.isPinSet) { + router.push(TangemPayDetailsInnerRoute.ChangePIN) + } else { + bottomSheetNavigation.activate( + TangemPayDetailsNavigation.ViewPinCode( + userWalletId = params.userWalletId, + cardId = params.config.cardId, + ), + ) + } } override fun onClickFreezeCard() { @@ -389,6 +399,15 @@ internal class TangemPayDetailsModel @Inject constructor( bottomSheetNavigation.dismiss() } + override fun onClickChangePin() { + bottomSheetNavigation.dismiss() + router.push(TangemPayDetailsInnerRoute.ChangePIN) + } + + override fun onDismissViewPin() { + bottomSheetNavigation.dismiss() + } + override fun onTransactionClick(item: TangemPayTxHistoryItem) { bottomSheetNavigation.activate( configuration = TangemPayDetailsNavigation.TransactionDetails( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt new file mode 100644 index 0000000000..14631dc022 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt @@ -0,0 +1,59 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.features.tangempay.components.TangemPayViewPinComponent +import com.tangem.features.tangempay.entity.TangemPayViewPinUM +import com.tangem.features.tangempay.model.transformers.TangemPayViewPinErrorStateTransformer +import com.tangem.features.tangempay.model.transformers.TangemPayViewPinSuccessStateTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayViewPinModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val cardDetailsRepository: TangemPayCardDetailsRepository, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + init { + getCardPin() + } + + private fun getCardPin() { + modelScope.launch { + cardDetailsRepository.getPin(userWalletId = params.walletId, cardId = params.cardId) + .onRight { pin -> + if (!pin.isNullOrEmpty()) { + uiState.update(TangemPayViewPinSuccessStateTransformer(pin, params.listener::onClickChangePin)) + } else { + uiState.update(TangemPayViewPinErrorStateTransformer()) + } + } + .onLeft { + uiState.update(TangemPayViewPinErrorStateTransformer()) + } + } + } + + private fun getInitialState(): TangemPayViewPinUM { + return TangemPayViewPinUM.Loading(onDismiss = ::onDismiss) + } + + fun onDismiss() { + params.listener.onDismissViewPin() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt new file mode 100644 index 0000000000..eacc9656fd --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.core.ui.R +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.secondaryButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage +import com.tangem.features.tangempay.entity.TangemPayViewPinUM +import com.tangem.utils.transformer.Transformer + +internal class TangemPayViewPinErrorStateTransformer : Transformer { + + override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM { + return TangemPayViewPinUM.Error( + errorMessage = bottomSheetMessage { + infoBlock { + icon(R.drawable.img_attention_20) { + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention + } + title = TextReference.Res(R.string.common_error) + body = TextReference.Res(R.string.common_unknown_error) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + }.messageBottomSheetUMV2, + onDismiss = prevState.onDismiss, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt new file mode 100644 index 0000000000..9258baaf5a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.features.tangempay.entity.TangemPayViewPinUM +import com.tangem.utils.transformer.Transformer + +internal class TangemPayViewPinSuccessStateTransformer( + private val pin: String, + private val onClickChangePin: () -> Unit, +) : Transformer { + + override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM { + return TangemPayViewPinUM.Content( + pin = pin, + onClickChangePin = onClickChangePin, + onDismiss = prevState.onDismiss, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt new file mode 100644 index 0000000000..ece1240de8 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt @@ -0,0 +1,49 @@ +package com.tangem.features.tangempay.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +internal fun PinDigitBox( + digit: String?, + backgroundColor: Color, + borderColor: Color, + textColor: Color, + textStyle: TextStyle, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(width = 48.dp, height = 64.dp) + .background( + color = backgroundColor, + shape = RoundedCornerShape(12.dp), + ) + .border( + width = 1.dp, + color = borderColor, + shape = RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + if (digit != null) { + Text( + text = digit, + style = textStyle, + color = textColor, + textAlign = TextAlign.Center, + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt index f052057177..c1eaca6abc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt @@ -1,11 +1,8 @@ package com.tangem.features.tangempay.ui import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -17,11 +14,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -187,38 +182,4 @@ private fun PinCode( } }, ) -} - -@Composable -private fun PinDigitBox( - digit: String?, - backgroundColor: Color, - borderColor: Color, - textColor: Color, - textStyle: TextStyle, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .size(width = 48.dp, height = 64.dp) - .background( - color = backgroundColor, - shape = RoundedCornerShape(12.dp), - ) - .border( - width = 1.dp, - color = borderColor, - shape = RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, - ) { - if (digit != null) { - Text( - text = digit, - style = textStyle, - color = textColor, - textAlign = TextAlign.Center, - ) - } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt new file mode 100644 index 0000000000..bcd40588a5 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt @@ -0,0 +1,204 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Transparent +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2Content +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayViewPinUM + +@Composable +internal fun TangemPayViewPinContent(state: TangemPayViewPinUM) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = state.onDismiss, + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = TextReference.EMPTY, + endIconRes = R.drawable.ic_close_24, + onEndClick = state.onDismiss, + ) + }, + ) { + when (state) { + is TangemPayViewPinUM.Content -> { + PinSuccessContent(state) + } + is TangemPayViewPinUM.Error -> { + MessageBottomSheetV2Content(state.errorMessage) + } + is TangemPayViewPinUM.Loading -> { + PinLoadingContent() + } + } + } +} + +@Composable +private fun PinSuccessContent(state: TangemPayViewPinUM.Content, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH12() + + Text( + text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + SpacerH24() + + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center, + ) { + PinCode( + modifier = Modifier + .align(Alignment.TopCenter), + value = state.pin, + ) + + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.tangempay_change_pin_code), + onClick = state.onClickChangePin, + ) + } + } +} + +@Composable +private fun PinLoadingContent(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH12() + + Text( + text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + SpacerH24() + + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.text.disabled, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 16.dp) + .size(TangemTheme.dimens.size24), + ) + } + } +} + +@Composable +private fun PinCode(value: String, modifier: Modifier = Modifier, numbersCount: Int = 4) { + BasicTextField( + value = value, + onValueChange = {}, + modifier = modifier, + textStyle = TangemTheme.typography.h1.copy(color = Transparent), + cursorBrush = SolidColor(Transparent), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(numbersCount) { index -> + val digit = value.getOrNull(index)?.toString() + PinDigitBox( + digit = digit, + backgroundColor = TangemTheme.colors.background.action, + borderColor = TangemTheme.colors.stroke.primary, + textColor = TangemTheme.colors.text.primary1, + textStyle = TangemTheme.typography.h1, + ) + } + } + innerTextField() + } + }, + ) +} + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayViewPinContentPreview() { + TangemThemePreview { + TangemPayViewPinContent( + state = TangemPayViewPinUM.Content( + pin = "1234", + onClickChangePin = {}, + onDismiss = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index 912bfefbd5..9e96a75104 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -7,7 +7,7 @@ internal interface TangemPayDetailIntents { fun onRefreshSwipe(refreshState: ShowRefreshState) fun onClickAddFunds() fun onClickWithdraw() - fun onClickChangePin() + fun onClickPinCode() fun onClickFreezeCard() fun onClickUnfreezeCard() fun onClickTermsAndLimits() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index f9127d04ef..a47a0aec3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer( openDetails( TangemPayDetailsConfig( cardId = productInstance.cardId, + isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits, From 2db9958dd119f08c364f6ea0814db04c443e3b93 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 20:56:27 +0400 Subject: [PATCH 097/130] Updated on 2026-08-14 --- .../models/response/CardBalanceResponse.kt | 1 - .../models/response/CardDetailsResponse.kt | 1 - .../response/CheckCustomerWalletResponse.kt | 1 - .../response/CustomerEligibilityResponse.kt | 1 - .../pay/models/response/CustomerMeResponse.kt | 1 - .../response/DeeplinkValidityResponse.kt | 1 - .../response/FreezeUnfreezeCardResponse.kt | 1 - .../api/pay/models/response/OrderResponse.kt | 1 - .../response/TangemPayTxHistoryResponse.kt | 1 - .../models/response/WithdrawDataResponse.kt | 1 - .../pay/models/response/WithdrawResponse.kt | 1 - .../pay/DefaultTangemPayEligibilityManager.kt | 54 +++++++++-- .../domain/pay/TangemPayEligibilityManager.kt | 2 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 32 ++++--- .../features/details/model/DetailsModel.kt | 4 +- .../model/TangemPayOnboardingModel.kt | 11 ++- .../visa/TangemPayOnboardingBanner.kt | 89 +++++++++---------- 17 files changed, 120 insertions(+), 83 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt index 164915f64d..6eae94e330 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt @@ -4,5 +4,4 @@ import com.squareup.moshi.Json data class CardBalanceResponse( @Json(name = "result") val result: BalanceResponse?, - @Json(name = "error") val error: String?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt index 007bcf7428..17b226bacc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt @@ -5,7 +5,6 @@ import com.squareup.moshi.JsonClass data class CardDetailsResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt index b926f0fa05..01ee25582c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CheckCustomerWalletResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { data class Result( @Json(name = "id") val id: String?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt index 8ddcfab3f7..48e148069c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CustomerEligibilityResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index cc0edd82a4..69405843f7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CustomerMeResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt index 638055ef5c..71b094a2cd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class DeeplinkValidityResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt index 7ec1155bdc..e42230d2fc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class FreezeUnfreezeCardResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index 806012dfa3..a476b5dc1b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class OrderResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt index 8b75b8634b..009b96d99c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt @@ -7,7 +7,6 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class TangemPayTxHistoryResponse( - @Json(name = "error") val error: String?, @Json(name = "result") val result: Result, ) { @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt index 8bc4fcbf1e..af5860ab39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WithdrawDataResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt index 2b347f27c0..5a353ae6bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt @@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WithdrawResponse( @Json(name = "result") val result: Result?, - @Json(name = "error") val error: String?, ) { @JsonClass(generateAdapter = true) data class Result( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 97a6d9e7ca..f2d850f463 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -9,26 +9,43 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import javax.inject.Inject internal class DefaultTangemPayEligibilityManager @Inject constructor( + dispatchers: CoroutineDispatcherProvider, private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val onboardingRepository: OnboardingRepository, ) : TangemPayEligibilityManager { - private var cachedEligibleWallets: List? = null - private var eligibleWalletsDeferred: Deferred>? = null + private var cachedEligibleWallets: List? = null + private var eligibleWalletsDeferred: Deferred>? = null private val loadMutex = Mutex() + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default) - override suspend fun getEligibleWallets(): List { + init { + resetDataWhenWalletsUpdate() + } + + override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List { + return getUserWalletsData().mapNotNull { + if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null + } + } + + private suspend fun getUserWalletsData(): List { cachedEligibleWallets?.let { return it } return loadMutex.withLock { @@ -38,7 +55,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( coroutineScope { val deferred = async { getPossibleWalletsForTangemPay() - .excludePaeraCustomers() + .addPaeraCustomersData() .also { cachedEligibleWallets = it } } eligibleWalletsDeferred = deferred @@ -73,8 +90,8 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( is UserWallet.Hot -> true } - private suspend fun List.excludePaeraCustomers(): List { - if (isEmpty()) return this + private suspend fun List.addPaeraCustomersData(): List { + if (isEmpty()) return emptyList() return coroutineScope { map { wallet -> @@ -86,9 +103,30 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } .awaitAll() - .mapNotNull { (wallet, isCustomer) -> - wallet.takeUnless { isCustomer } + .map { (userWallet, isPaeraCustomer) -> + UserWalletData(userWallet, isPaeraCustomer) } } } + + private fun resetDataWhenWalletsUpdate() { + coroutineScope.launch { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.collectLatest { reset() } + } else { + userWalletsListManager.userWallets.collectLatest { reset() } + } + } + } + + private fun reset() { + cachedEligibleWallets = null + eligibleWalletsDeferred?.cancel() + eligibleWalletsDeferred = null + } + + private data class UserWalletData( + val userWallet: UserWallet, + val isPaeraCustomer: Boolean, + ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt index b68cd25513..5ad9a9d3d9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -4,5 +4,5 @@ import com.tangem.domain.models.wallet.UserWallet interface TangemPayEligibilityManager { - suspend fun getEligibleWallets(): List + suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 9636a5136b..16ebd064e6 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -46,7 +46,11 @@ class TangemPayMainScreenCustomerInfoUseCase( .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + if (error is VisaApiError.NotPaeraCustomer) { + showOnboardingBannerIfEligible(userWalletId) + } else { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } }, ifRight = { hasTangemPay -> Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") @@ -61,21 +65,27 @@ class TangemPayMainScreenCustomerInfoUseCase( updateState(userWalletId, result) } else { // if there's no tangem pay, check eligibility and show onboarding banner - val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId } - if (isEligible) { - if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) { - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) - } else { - updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) - } - } else { - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) - } + showOnboardingBannerIfEligible(userWalletId) } }, ) } + private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) { + val isEligible = eligibilityManager + .getEligibleWallets(shouldExcludePaeraCustomers = false) + .any { it.walletId == userWalletId } + if (isEligible) { + if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } else { + updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) + } + } else { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } + } + operator fun invoke( userWalletId: UserWalletId, ): Flow> { 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 69fe323b69..f5d5d79eb5 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 @@ -248,7 +248,9 @@ internal class DetailsModel @Inject constructor( private fun addTangemPayItemIfEligible() { if (!tangemPayFeatureToggles.isTangemPayEnabled) return modelScope.launch { - val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty() + val isEligible = tangemPayEligibilityManager + .getEligibleWallets(shouldExcludePaeraCustomers = true) + .isNotEmpty() if (isEligible) { items.update { itemsBuilder.addVisaItem(it) } } 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 764ff69042..83700ab313 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 @@ -65,7 +65,9 @@ internal class TangemPayOnboardingModel @Inject constructor( .onRight { isValid -> if (isValid) showOnboarding() else back() } .onLeft { back() } } - is TangemPayOnboardingComponent.Params.ContinueOnboarding, + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { + openKyc(userWalletId = params.userWalletId) + } is TangemPayOnboardingComponent.Params.FromBannerInSettings, is TangemPayOnboardingComponent.Params.FromBannerOnMain, -> showOnboarding() @@ -113,8 +115,13 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun onGetCardClick() { analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) + // if user came from deeplink or banner in settings and already is a paera customer -> exclude this wallet + val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings || + params is TangemPayOnboardingComponent.Params.Deeplink modelScope.launch { - val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId } + val eligibleWalletsIds = eligibilityManager + .getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers) + .map { it.walletId } if (eligibleWalletsIds.isEmpty()) { back() return@launch diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt index b57061cb36..62e59a5eed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt @@ -4,21 +4,9 @@ import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset @@ -28,8 +16,9 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -57,28 +46,20 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m ) .clickable(onClick = state.onClick), ) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, + ConstraintLayout( + modifier = Modifier.fillMaxWidth(), ) { - Image( - painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner), - contentDescription = null, - modifier = Modifier - .padding(horizontal = 8.dp) - .width(55.dp) - .height(95.dp) - .offset(y = 10.dp), - ) - - SpacerH4() + val (image, text, close) = createRefs() Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, + modifier = Modifier + .constrainAs(text) { + top.linkTo(parent.top) + start.linkTo(image.end, margin = 12.dp) + end.linkTo(parent.end) + width = Dimension.fillToConstraints + } + .padding(top = 16.dp, end = 16.dp, bottom = 16.dp), ) { Text( text = stringResourceSafe(R.string.tangempay_onboarding_banner_title), @@ -94,22 +75,32 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m color = TangemTheme.colors.text.tertiary, ) } - Box( - modifier = Modifier.fillMaxHeight(), - contentAlignment = Alignment.TopEnd, - ) { - Image( - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .clickable(onClick = state.closeOnClick) - .padding(4.dp) - .padding(top = 12.dp) - .size(12.dp), - painter = painterResource(id = R.drawable.ic_close_24), - colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), - contentDescription = null, - ) - } + + Image( + painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner), + contentDescription = null, + modifier = Modifier + .constrainAs(image) { + start.linkTo(parent.start) + top.linkTo(text.top) + bottom.linkTo(text.bottom) + height = Dimension.fillToConstraints + } + .padding(top = 8.dp, start = 24.dp), + ) + + Image( + painter = painterResource(R.drawable.ic_close_24), + contentDescription = null, + modifier = Modifier + .size(16.dp) + .clickable(onClick = state.closeOnClick) + .constrainAs(close) { + top.linkTo(parent.top, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + }, + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + ) } } } From 7b9cf3f4e190ef91a9ba325a94036efb530e71bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 21:58:34 +0500 Subject: [PATCH 098/130] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 81 +++++++++++-------- .../analytics/WalletScreenAnalyticsEvent.kt | 2 + 2 files changed, 49 insertions(+), 34 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 b136451f60..c4b8aeb87c 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 @@ -121,6 +121,8 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { + trackScreenOpened() + updateMarketToggle() suggestToOpenMarkets() @@ -139,9 +141,7 @@ internal class WalletModel @Inject constructor( } fun onResume() { - modelScope.launch(dispatchers.main) { - suggestToEnableBiometrics() - } + suggestToEnableBiometrics() } private fun updateMarketToggle() { @@ -178,13 +178,15 @@ internal class WalletModel @Inject constructor( walletScreenContentLoader.cancelAll() } - private suspend fun suggestToEnableBiometrics() { - if (shouldShowAskBiometryBottomSheet()) { - delay(timeMillis = 1_800) + private fun suggestToEnableBiometrics() { + modelScope.launch(dispatchers.main) { + if (shouldShowAskBiometryBottomSheet()) { + delay(timeMillis = 1_800) - innerWalletRouter.dialogNavigation.activate( - configuration = WalletDialogConfig.AskForBiometry, - ) + innerWalletRouter.dialogNavigation.activate( + configuration = WalletDialogConfig.AskForBiometry, + ) + } } } @@ -202,6 +204,42 @@ internal class WalletModel @Inject constructor( } } + private fun trackScreenOpened() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + modelScope.launch { + userWalletsListRepository + .selectedUserWalletSync() + ?.let { selectedWallet -> + val hasMobileWallet = userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot } + + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(selectedWallet.walletId) + .first() + .accounts + .size + } else { + null + } + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + hasMobileWallet = hasMobileWallet, + accountsCount = accountsCount, + theme = theme.value, + isImported = selectedWallet.isImported(), + ), + ) + } + } + } else { + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy(), + ) + } + } + private suspend fun shouldShowAskBiometryBottomSheet(): Boolean { return if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && @@ -289,31 +327,6 @@ internal class WalletModel @Inject constructor( .onEach { selectedWallet -> trackingContextProxy.setContext(selectedWallet) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - modelScope.launch { - val hasMobileWallet = userWalletsListRepository.userWalletsSync() - .any { it is UserWallet.Hot } - val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { - singleAccountListSupplier(selectedWallet.walletId) - .first() - .accounts - .size - } else { - null - } - val result = getAppThemeModeUseCase().firstOrNull() - val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM - analyticsEventsHandler.send( - WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( - hasMobileWallet = hasMobileWallet, - accountsCount = accountsCount, - theme = theme.value, - isImported = selectedWallet.isImported(), - ), - ) - } - } - if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } 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 dec57f0b23..c54ebcef16 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,6 +51,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { + class ScreenOpenedLegacy : MainScreen(event = "Screen opened") + data class ScreenOpened( private val hasMobileWallet: Boolean, private val accountsCount: Int?, From c707bef9a3a7c8af8622c93acc7ff9ffe8909734 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 09:41:07 +0200 Subject: [PATCH 099/130] Updated on 2026-08-14 --- .../data/staking/DefaultP2PEthPoolRepository.kt | 11 +++++++++-- .../com/tangem/data/staking/di/StakingDataModule.kt | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index 85f3038652..1f2f75086b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -17,6 +17,7 @@ import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.staking.model.ethpool.* import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -31,6 +32,7 @@ internal class DefaultP2PEthPoolRepository( private val p2pApi: P2PEthPoolApi, private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggles: StakingFeatureToggles, ) : P2PEthPoolRepository { private val vaultConverter = P2PEthPoolVaultConverter @@ -40,10 +42,15 @@ internal class DefaultP2PEthPoolRepository( private val errorConverter = P2PEthPoolErrorConverter override suspend fun fetchVaults(network: P2PEthPoolNetwork) { - val vaults = getVaults(network).getOrElse { error -> - Timber.e("Error fetching P2P vaults: $error") + val vaults = if (stakingFeatureToggles.isEthStakingEnabled) { + getVaults(network).getOrElse { error -> + Timber.e("Error fetching P2P vaults: $error") + emptyList() + } + } else { emptyList() } + p2pEthPoolVaultsStore.store(vaults) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 22164269fd..95f7430209 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -78,11 +78,13 @@ internal object StakingDataModule { p2pApi: P2PEthPoolApi, p2pEthPoolVaultsStore: P2PEthPoolVaultsStore, dispatchers: CoroutineDispatcherProvider, + stakingFeatureToggles: StakingFeatureToggles, ): P2PEthPoolRepository { return DefaultP2PEthPoolRepository( p2pApi = p2pApi, p2pEthPoolVaultsStore = p2pEthPoolVaultsStore, dispatchers = dispatchers, + stakingFeatureToggles = stakingFeatureToggles, ) } From 9beb68cc03817b731a2406d290cf85fd937de985 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 13:29:39 +0300 Subject: [PATCH 100/130] Updated on 2026-08-14 --- .../accesscode/AccessCodeComponent.kt | 1 + .../hotwallet/accesscode/AccessCodeModel.kt | 94 ++++++++++++------- .../accesscode/entity/AccessCodeUM.kt | 1 + .../hotwallet/accesscode/ui/AccessCode.kt | 57 +++++++++++ .../entry/AddExistingWalletModel.kt | 11 ++- .../updateaccesscode/UpdateAccessCodeModel.kt | 4 + .../entry/WalletActivationModel.kt | 11 ++- 7 files changed, 142 insertions(+), 37 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index 08693225cc..b9badf2e25 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -53,6 +53,7 @@ internal class AccessCodeComponent @AssistedInject constructor( interface ModelCallbacks { fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) + fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) fun onAccessCodeUpdated(userWalletId: UserWalletId) } 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 13f1a7ab8f..1da4a1c296 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 @@ -16,6 +16,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireHotWallet import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.SetAskBiometryShownUseCase import com.tangem.domain.settings.ShouldShowAskBiometryUseCase @@ -31,6 +32,7 @@ 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.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -80,6 +82,7 @@ internal class AccessCodeModel @Inject constructor( accessCode = "", accessCodeColor = PinTextColor.Primary, onAccessCodeChange = ::onAccessCodeChange, + isLoading = false, isConfirmMode = params.accessCodeToConfirm != null, ) @@ -117,6 +120,18 @@ internal class AccessCodeModel @Inject constructor( } } + private suspend fun setLoadingIfLongJob(job: Job) { + delay(timeMillis = 2000) + + if (job.isActive) { + uiState.update { currentState -> + currentState.copy( + isLoading = true, + ) + } + } + } + private fun onNewCodeSet() { modelScope.launch { delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS) @@ -187,56 +202,65 @@ internal class AccessCodeModel @Inject constructor( } private fun setCode(userWalletId: UserWalletId, accessCode: String) { + params.callbacks.onAccessCodeUpdateStarted(params.userWalletId) + modelScope.launch { val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet with id $userWalletId not found") } - if (userWallet !is UserWallet.Hot) return@launch + .requireHotWallet() tryToAskForBiometry() - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), - ) - - if (walletsRepository.useBiometricAuthentication()) { - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + val settingCodeJob = launch(dispatchers.main) { + setCodeOperation(userWallet, accessCode) + params.callbacks.onAccessCodeUpdated(params.userWalletId) } - val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId) - .getOrNull() - ?: run { - require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) { - "Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth" - } + setLoadingIfLongJob(settingCodeJob) + } + } - hotWalletAccessor.unlockContextual(userWallet.hotWalletId) + private suspend fun setCodeOperation(userWallet: UserWallet.Hot, accessCode: String) { + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + ) + } + + val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId) + .getOrNull() + ?: run { + require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) { + "Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth" } - var updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Password(accessCode.toCharArray()), - ) - - if (walletsRepository.requireAccessCode().not()) { - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Biometry, - ) + hotWalletAccessor.unlockContextual(userWallet.hotWalletId) } - userWalletsListRepository.saveWithoutLock( - userWallet.copy(hotWalletId = updatedHotWalletId), - canOverride = true, + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Biometry, ) - - clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) - - params.callbacks.onAccessCodeUpdated(params.userWalletId) } + + userWalletsListRepository.saveWithoutLock( + userWallet.copy(hotWalletId = updatedHotWalletId), + canOverride = true, + ) + + clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 978071789b..5b4302d4bf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -8,6 +8,7 @@ import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( val accessCode: String, val accessCodeColor: PinTextColor, + val isLoading: Boolean, val onAccessCodeChange: (String) -> Unit, val isConfirmMode: Boolean, val requestFocus: StateEvent = consumedEvent(), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index ecfa44d9d6..f92f63bdfe 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -3,8 +3,10 @@ package com.tangem.features.hotwallet.accesscode.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -16,9 +18,14 @@ import com.tangem.core.res.R import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Suppress("LongParameterList", "LongMethod") @Composable @@ -27,6 +34,24 @@ internal fun AccessCode( modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, ) { + if (state.isLoading) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .navigationBarsPadding(), + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(20.dp), + color = TangemTheme.colors.icon.secondary, + strokeWidth = 2.dp, + ) + } + return + } + Column( modifier = modifier .fillMaxSize() @@ -88,6 +113,20 @@ internal fun AccessCode( } } } + + val hapticManager = LocalHapticManager.current + + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + launch(NonCancellable) { + delay(timeMillis = 500) + hapticManager.perform(TangemHapticEffect.View.Reject) + } + } + else -> Unit + } + } } @Preview(showBackground = true, widthDp = 360) @@ -101,6 +140,7 @@ private fun PreviewSet() { accessCodeColor = PinTextColor.Primary, onAccessCodeChange = {}, isConfirmMode = false, + isLoading = false, ), ) } @@ -117,6 +157,23 @@ private fun PreviewConfirm() { accessCodeColor = PinTextColor.Success, onAccessCodeChange = {}, isConfirmMode = true, + isLoading = false, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun PreviewConfirmLoading() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "123456", + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + isConfirmMode = true, + isLoading = true, ), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index ed1d37d3f9..dc5afd1b9f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -123,12 +123,16 @@ internal class AddExistingWalletModel @Inject constructor( } inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + var canSkip = true + override fun onBackClick() { onChildBack() } override fun onSkipClick() { - showSkipAccessCodeWarningDialog() + if (canSkip) { + showSkipAccessCodeWarningDialog() + } } } @@ -157,7 +161,12 @@ internal class AddExistingWalletModel @Inject constructor( stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = false + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = true navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt index cd562b219b..e671521f5d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -50,6 +50,10 @@ internal class UpdateAccessCodeModel @Inject constructor( stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + // No-op + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { stackNavigation.push(UpdateAccessCodeRoute.SetupFinished) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 129947fdb7..2a1f014021 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -156,12 +156,16 @@ internal class WalletActivationModel @Inject constructor( } inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + var canSkip = true + override fun onBackClick() { onChildBack() } override fun onSkipClick() { - showSkipAccessCodeWarningDialog() + if (canSkip) { + showSkipAccessCodeWarningDialog() + } } } @@ -220,8 +224,13 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = false + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { navigateToPushNotificationsOrNext() + hotWalletStepperComponentModelCallback.canSkip = true } } From 228c0c8af40db539ebe0caeab46137fe5ee05d3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 14:52:24 +0300 Subject: [PATCH 101/130] Updated on 2026-08-14 --- .../java/com/tangem/tap/domain/TapWalletManager.kt | 3 +++ .../com/tangem/tap/features/main/MainViewModel.kt | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 72762cb7a0..f486cce85c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -33,6 +33,9 @@ class TapWalletManager( .apply { join() } } + /** + * [REDACTED_TODO_COMMENT] + */ private suspend fun loadUserWalletData(userWallet: UserWallet) { val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy) trackingContextProxy.setContext(userWallet) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index d70fc8ae08..9ea074747f 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.keyboard.KeyboardValidator import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent -import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R @@ -81,7 +80,6 @@ internal class MainViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val appRouterConfig: AppRouterConfig, private val sellService: SellService, - private val trackingContextProxy: TrackingContextProxy, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -143,7 +141,7 @@ internal class MainViewModel @Inject constructor( } } - prepareSelectedWalletFeedback() + subscribeToSelectedWallet() // await while initial route stack is initialized appRouterConfig.initializedState.first { it } @@ -172,12 +170,15 @@ internal class MainViewModel @Inject constructor( } } - private fun prepareSelectedWalletFeedback() { + private fun subscribeToSelectedWallet() { getSelectedWalletUseCase.invoke() .mapLeft { emptyFlow() } .onRight { wallet -> wallet.distinctUntilChanged() - .onEach { trackingContextProxy.setContext(it) } + .onEach { + // FIXME Do not remove this call without checking implications !!! + appStateHolder.onUserWalletSelected(it) + } .flowOn(dispatchers.io) .launchIn(viewModelScope) } From 82fa3edf458f9bf7b40a491a9c83fc5d08539eba Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 14:16:53 +0300 Subject: [PATCH 102/130] Updated on 2026-08-14 --- .../com/tangem/common/TangemBlogUrlBuilder.kt | 23 +++++----- .../com/tangem/common/TangemSiteUrlBuilder.kt | 42 +++++++++++++++++++ .../com/tangem/utils/SupportedLanguages.kt | 2 +- .../com/tangem/utils/TangemBlogUrlBuilder.kt | 1 + domain/wallets/build.gradle.kts | 2 + .../GenerateBuyTangemCardLinkUseCase.kt | 29 ++++++------- .../CreateWalletStartModel.kt | 3 +- .../features/details/model/DetailsModel.kt | 4 +- .../features/home/impl/model/HomeModel.kt | 2 +- .../CreateHardwareWalletModel.kt | 3 +- .../upgradewallet/UpgradeWalletModel.kt | 3 +- .../walletbackup/model/WalletBackupModel.kt | 3 +- .../model/WalletHardwareBackupModel.kt | 3 +- features/onboarding-v2/impl/build.gradle.kts | 1 + .../model/MultiWalletSeedPhraseModel.kt | 7 +++- .../impl/child/seedphrase/model/Utils.kt | 28 ------------- .../tangem/feature/swap/ui/StateBuilder.kt | 1 + .../model/WcSendTransactionModel.kt | 1 + 18 files changed, 97 insertions(+), 61 deletions(-) create mode 100644 common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 4966d2d7b0..0319e1c59f 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -1,20 +1,15 @@ package com.tangem.common -import com.tangem.utils.SupportedLanguages - /** [REDACTED_AUTHOR] */ object TangemBlogUrlBuilder { - fun build(post: Post): String { - val code = SupportedLanguages.getCurrentSupportedLanguageCode() - .takeIf { code -> - code == SupportedLanguages.RUSSIAN || code == SupportedLanguages.ENGLISH - } - ?: SupportedLanguages.ENGLISH - - return "https://tangem.com/$code/blog/post/${post.path}/" + suspend fun build(post: Post): String { + return TangemSiteUrlBuilder.url( + path = "/blog/post/${post.path}/", + campaign = "articles", + ) } sealed interface Post { @@ -28,5 +23,13 @@ object TangemBlogUrlBuilder { data object SeedNotifySecond : Post { override val path: String = "tangem-resolves-log-issue" } + + data object SeedPhraseRiskySolution : Post { + override val path: String = "seed-phrase-a-risky-solution" + } + + data object WhatWalletToChoose : Post { + override val path: String = "mobile-wallet" + } } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt new file mode 100644 index 0000000000..573b95bdfb --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -0,0 +1,42 @@ +package com.tangem.common + +import android.content.res.Resources +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import java.util.Locale +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object TangemSiteUrlBuilder { + + suspend fun getUtmTags(campaign: String?): String { + val langCode = Locale.getDefault().language + val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() + val utmContent = deviceLang()?.let { "&utm_content=devicelang-$it" }.orEmpty() + val appInstanceIdPart = getAppInstanceId()?.let { "&app_instance_id=$it" }.orEmpty() + return "utm_source=tangem-app&utm_medium=app$utmCampaignPart$utmContent$appInstanceIdPart" + } + + suspend fun url(path: String, campaign: String): String { + val normalizedPath = path.trim('/') + return "https://tangem.com/$normalizedPath?${getUtmTags(campaign)}" + } + + private fun deviceLang(): String? { + return runCatching { + Resources.getSystem().configuration.locales.get(0).toLanguageTag() + }.getOrNull() + } + + private suspend fun getAppInstanceId(): String? { + return suspendCoroutine { cont -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { id -> + cont.resume(id) + } + .addOnFailureListener { + cont.resume(null) + } + } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt index b85a35b477..420547d13c 100644 --- a/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt +++ b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt @@ -10,7 +10,7 @@ object SupportedLanguages { const val ITALIAN = "it" const val JAPANESE = "ja" const val UKRAINIAN = "uk" - const val CHINESE = "uk" + const val CHINESE = "zh" const val SPANISH = "es" val supportedLangugeCodes = listOf( diff --git a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt index 5d8f4ba63f..d24267a527 100644 --- a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt +++ b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt @@ -2,6 +2,7 @@ package com.tangem.utils import java.util.Locale +@Deprecated("Use TangemBlogUrlBuilder from common module") object TangemBlogUrlBuilder { private const val RU_LOCALE = "ru" diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index a5779a9b3f..b7b3798d96 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation(projects.domain.hotWallet) // endregion + implementation(projects.common) + // region Tangem libraries implementation(tangemDeps.blockchain) // android-library implementation(tangemDeps.card.core) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt index 7c80707cb6..7a7cac1d2b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -1,23 +1,24 @@ package com.tangem.domain.wallets.usecase -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine +import com.tangem.common.TangemSiteUrlBuilder +import java.util.Locale class GenerateBuyTangemCardLinkUseCase { - suspend operator fun invoke(): String = suspendCoroutine { cont -> - Firebase.analytics.appInstanceId - .addOnSuccessListener { id -> - cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id") - } - .addOnFailureListener { - cont.resume(NEW_BUY_WALLET_URL) - } + suspend operator fun invoke(source: Source): String { + return invoke(source.utmCampaign) } - companion object { - private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + suspend operator fun invoke(utmCampaign: String?): String { + val langCode = Locale.getDefault().language + val utmTags = TangemSiteUrlBuilder.getUtmTags(utmCampaign) + return "https://buy.tangem.com/$langCode?$utmTags" + } + + enum class Source(val utmCampaign: String) { + Creation("prospect"), + Settings("users"), + Backup("backup"), + Upgrade("upgrade"), } } \ No newline at end of file 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 ef8ef65b13..8c7bb0d2eb 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 @@ -162,7 +162,8 @@ internal class CreateWalletStartModel @Inject constructor( private fun onBuyClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation).let { urlOpener.openUrl(it) } } } 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 f5d5d79eb5..b730125b22 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 @@ -232,8 +232,10 @@ internal class DetailsModel @Inject constructor( modelScope.launch { if (hotWalletFeatureToggles.isHotWalletEnabled) { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings)) - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Settings).let { urlOpener.openUrl(it) } } else { + // This is incorrect implementation of buy link generation, but it is left here 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 1dbc4c7a1f..e650912dc6 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 @@ -135,7 +135,7 @@ internal class HomeModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) analyticsEventHandler.send(Shop.ScreenOpened()) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase.invoke(null).let { urlOpener.openUrl(it) } } } 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 2d4b71b076..0373179468 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 @@ -78,7 +78,8 @@ internal class CreateHardwareWalletModel @Inject constructor( private fun onBuyTangemWalletClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWallet)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Upgrade).let { urlOpener.openUrl(it) } } } 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 7b675b8238..c7252d49c4 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 @@ -94,7 +94,8 @@ internal class UpgradeWalletModel @Inject constructor( private fun onBuyTangemWalletClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Upgrade).let { urlOpener.openUrl(it) } } } 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 f1a729835a..bdc95378ca 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 @@ -132,7 +132,8 @@ internal class WalletBackupModel @Inject constructor( private fun onBuyClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).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 31d5ebdf25..efcd39aa14 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 @@ -171,7 +171,8 @@ internal class WalletHardwareBackupModel @Inject constructor( private fun onBuyClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).let { urlOpener.openUrl(it) } } } diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 7febfb81ba..a992e7daea 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) + implementation(projects.common) /** Domain */ implementation(projects.domain.models) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 0f18033dd3..3468f7a11c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.mode import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.CompletionResult +import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -161,7 +162,11 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( openGeneratedSeedPhrase() }, onLearnMoreClicked = { - urlOpener.openUrl(seedPhraseLearnMoreUrl()) + modelScope.launch { + urlOpener.openUrl( + TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.SeedPhraseRiskySolution), + ) + } }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt deleted file mode 100644 index dbfeee4003..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model - -import android.net.Uri -import com.tangem.utils.SupportedLanguages -import java.util.Locale - -// TODO: [REDACTED_JIRA] -internal fun seedPhraseLearnMoreUrl(): String { - val language = Locale.getDefault().language - - val languageBy = "by" - if (SupportedLanguages.RUSSIAN.equals(language, true) || - languageBy.equals(language, true) - ) { - SupportedLanguages.RUSSIAN - } else { - SupportedLanguages.ENGLISH - } - - val webUri = Uri.Builder() - .scheme("https") - .authority("tangem.com") - .appendPath(language) - .appendPath("blog/post/seed-phrase-a-risky-solution") - .build() - - return webUri.toString() -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 64ac9aa044..6ab1d4af06 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1194,6 +1194,7 @@ internal class StateBuilder( ) } + @Deprecated("Use TangemBlockUrlBuilder instead") private fun buildReadMoreUrl(): String { return buildString { append(FEE_READ_MORE_URL_FIRST_PART) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 611010e6f2..d305e4a233 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -297,6 +297,7 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pop() } + @Deprecated("Use TangemBlockUrlBuilder instead") private fun onApproveLearnMoreClick() { val code = SupportedLanguages.getCurrentSupportedLanguageCode() .takeIf { it == SupportedLanguages.RUSSIAN } From 30a8b879009f2b0460e9687640b273cdf26e70fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 14:29:01 +0300 Subject: [PATCH 103/130] Updated on 2026-08-14 --- .../impl/build.gradle.kts | 1 + .../CreateWalletSelectionModel.kt | 11 ++++++++++- .../entity/CreateWalletSelectionUM.kt | 1 + .../ui/CreateWalletSelectionContent.kt | 17 ++++++++++------- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 4f1fb170a2..458b4b5048 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) + implementation(projects.common) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) 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 2ee891e036..e3eab1b7e1 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 @@ -1,5 +1,6 @@ package com.tangem.features.createwalletselection +import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -84,6 +85,7 @@ internal class CreateWalletSelectionModel @Inject constructor( ), onBuyClick = ::onBuyClick, shouldShowAlreadyHaveWallet = true, + onWhatToChooseClick = ::onWhatToChooseClick, ), ) @@ -124,7 +126,14 @@ internal class CreateWalletSelectionModel @Inject constructor( private fun onBuyClick() { analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet)) modelScope.launch { - generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + generateBuyTangemCardLinkUseCase + .invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation).let { urlOpener.openUrl(it) } + } + } + + private fun onWhatToChooseClick() { + modelScope.launch { + urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatWalletToChoose)) } } diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt index 98cbe9a90b..2b1adb4ca1 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt @@ -10,6 +10,7 @@ internal data class CreateWalletSelectionUM( val blocks: ImmutableList, val onBackClick: () -> Unit, val onBuyClick: () -> Unit, + val onWhatToChooseClick: () -> Unit, ) { data class Block( val title: TextReference, diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index cddcad417a..37e1523bed 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -19,7 +19,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.TextButton import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM @@ -61,14 +63,14 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi }, title = { }, actions = { - Text( - modifier = Modifier - .padding(16.dp), + TextButton( + modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), text = stringResourceSafe(R.string.wallet_add_support_title), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + textStyle = TangemTheme.typography.body1, + colors = TangemButtonsDefaults.defaultTextButtonColors.copy( + contentColor = TangemTheme.colors.text.primary1, + ), + onClick = state.onWhatToChooseClick, ) }, ) @@ -288,6 +290,7 @@ private fun PreviewCreateWalletContent() { ), shouldShowAlreadyHaveWallet = true, onBuyClick = { }, + onWhatToChooseClick = { }, ), ) } From 6b1c110ed9dae0e6790e6062dfa414ce7b41ebcf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 12:46:44 +0000 Subject: [PATCH 104/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d167ffb2eb..462e99b59c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "releases-5.32-1329" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +tangemCardSdk = "releases-5.32-574" #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-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 6280be92689964ebd8036df8a0d751542d5b2149 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 11:32:11 +0300 Subject: [PATCH 105/130] Updated on 2026-08-14 --- .../CreateWalletSelectionModel.kt | 20 ------- .../entity/CreateWalletSelectionUM.kt | 6 -- .../ui/CreateWalletSelectionContent.kt | 60 ------------------- 3 files changed, 86 deletions(-) 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 e3eab1b7e1..14265dcd96 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 @@ -54,32 +54,12 @@ internal class CreateWalletSelectionModel @Inject constructor( style = LabelStyle.ACCENT, ), description = resourceReference(R.string.wallet_add_hardware_description), - features = persistentListOf( - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_add_wallet_16, - title = resourceReference(R.string.wallet_add_hardware_info_create), - ), - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_import_seed_16, - title = resourceReference(R.string.wallet_add_import_seed_phrase), - ), - ), onClick = ::onHardwareWalletClick, ), CreateWalletSelectionUM.Block( title = resourceReference(R.string.wallet_create_mobile_title), titleLabel = null, description = resourceReference(R.string.wallet_add_mobile_description), - features = persistentListOf( - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_mobile_wallet_16, - title = resourceReference(R.string.hw_create_title), - ), - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_import_seed_16, - title = resourceReference(R.string.wallet_add_import_seed_phrase), - ), - ), onClick = ::onMobileWalletClick, ), ), diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt index 2b1adb4ca1..685efc6da4 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt @@ -16,12 +16,6 @@ internal data class CreateWalletSelectionUM( val title: TextReference, val titleLabel: LabelUM?, val description: TextReference, - val features: ImmutableList, val onClick: () -> Unit, ) - - data class Feature( - val iconResId: Int, - val title: TextReference, - ) } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 37e1523bed..067a969771 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -33,7 +33,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.features.createwalletselection.impl.R -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Suppress("LongMethod") @@ -103,7 +102,6 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi .padding(top = 8.dp), title = block.title.resolveReference(), description = block.description.resolveReference(), - features = block.features, badge = block.titleLabel?.let { { Label(it) } }, @@ -126,7 +124,6 @@ private fun WalletBlock( title: String, description: String, onClick: () -> Unit, - features: ImmutableList, modifier: Modifier = Modifier, badge: @Composable (() -> Unit)? = null, ) { @@ -163,43 +160,6 @@ private fun WalletBlock( style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) - if (features.isNotEmpty()) { - HorizontalDivider( - modifier = Modifier.padding(top = 12.dp), - thickness = 0.5.dp, - color = TangemTheme.colors.stroke.primary, - ) - features.forEach { feature -> - Feature( - feature = feature, - modifier = Modifier - .padding(top = 12.dp), - ) - } - } - } -} - -@Composable -private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size16), - painter = painterResource(id = feature.iconResId), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - Text( - modifier = Modifier - .weight(1f, fill = false) - .padding(start = 6.dp), - text = feature.title.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) } } @@ -259,32 +219,12 @@ private fun PreviewCreateWalletContent() { style = LabelStyle.ACCENT, ), description = resourceReference(R.string.wallet_add_hardware_description), - features = persistentListOf( - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_add_wallet_16, - title = resourceReference(R.string.wallet_add_hardware_info_create), - ), - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_import_seed_16, - title = resourceReference(R.string.wallet_add_import_seed_phrase), - ), - ), onClick = { }, ), CreateWalletSelectionUM.Block( title = resourceReference(R.string.wallet_create_mobile_title), titleLabel = null, description = resourceReference(R.string.wallet_add_mobile_description), - features = persistentListOf( - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_mobile_wallet_16, - title = resourceReference(R.string.hw_create_title), - ), - CreateWalletSelectionUM.Feature( - iconResId = R.drawable.ic_import_seed_16, - title = resourceReference(R.string.wallet_add_import_seed_phrase), - ), - ), onClick = { }, ), ), From 8ea66c6e33e05d22c1032a3aed4b64ccc19be4af Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 14:00:21 +0500 Subject: [PATCH 106/130] Updated on 2026-08-14 --- .../java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index ccef106815..58df536d84 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -64,7 +64,7 @@ class TangemHotWalletSigner @AssistedInject constructor( hotWalletId = userWallet.hotWalletId, dataToSign = dataToSign.map { signData -> val wallet = - userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) } + userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) } ?: return CompletionResult.Failure( TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), ) From e904e548484560b93d634f5985ed8b25861816ff Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 14:00:49 +0500 Subject: [PATCH 107/130] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 4 ++++ features/hot-wallet/impl/build.gradle.kts | 1 - .../addexistingwallet/entry/AddExistingWalletModel.kt | 9 +++++---- .../walletactivation/entry/WalletActivationModel.kt | 9 +++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 690ab43218..076a21b682 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,6 +40,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase +import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -129,6 +130,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var cardRepository: CardRepository + @Inject + lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase + @Inject lateinit var backupServiceHolder: BackupServiceHolder diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 36d956f24c..380b51678e 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,7 +38,6 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) - implementation(projects.domain.notifications) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index dc5afd1b9f..cd8a7936ba 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -15,8 +15,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase -import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -25,6 +25,7 @@ import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow @@ -36,7 +37,7 @@ import javax.inject.Inject internal class AddExistingWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val notificationsRepository: NotificationsRepository, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @@ -78,8 +79,8 @@ internal class AddExistingWalletModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() - if (shouldAskNotificationPermissions) { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) } else { stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 2a1f014021..70cffd7a58 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase -import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent @@ -28,6 +28,7 @@ import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartCompone import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks @@ -43,7 +44,7 @@ internal class WalletActivationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val notificationsRepository: NotificationsRepository, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, @@ -117,8 +118,8 @@ internal class WalletActivationModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() - if (shouldAskNotificationPermissions) { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { stackNavigation.replaceAll(WalletActivationRoute.PushNotifications) } else { stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) From b2fbe74220ec3fd0eb3ddc33e01d36da4d4740dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 14:01:21 +0300 Subject: [PATCH 108/130] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 2 + .../amplitude/AmplitudeAnalyticsHandler.kt | 6 +-- .../appsflyer/AppsFlyerAnalyticsClient.kt | 19 ++++++++- .../appsflyer/AppsFlyerAnalyticsHandler.kt | 39 +++++++++++++------ .../handlers/appsflyer/AppsFlyerLogClient.kt | 8 ++++ .../firebase/FirebaseAnalyticsHandler.kt | 6 +-- .../handlers/firebase/FirebaseClient.kt | 2 +- ...t => UnderscoreAnalyticsEventConverter.kt} | 2 +- .../analytics/models/AppsFlyerOnlyEvent.kt | 15 +++++++ .../models/event/OnboardingAnalyticsEvent.kt | 12 +++++- .../core/analytics/api/EventHandlerApi.kt | 6 +-- .../analytics/filter/AppsFlyerEventFilter.kt | 23 +++++++++++ .../CreateMobileWalletModel.kt | 5 +-- .../v2/common/analytics/OnboardingEvent.kt | 8 +++- 14 files changed, 121 insertions(+), 32 deletions(-) rename app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/{FirebaseAnalyticsEventConverter.kt => UnderscoreAnalyticsEventConverter.kt} (95%) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/AppsFlyerOnlyEvent.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/filter/AppsFlyerEventFilter.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 2815df1738..abbd2d80de 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -19,6 +19,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.core.analytics.filter.AppsFlyerEventFilter import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam @@ -423,6 +424,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addFilter(oneTimeEventFilter) + factory.addFilter(AppsFlyerEventFilter()) val buildData = AnalyticsHandlerBuilder.Data( application = application, 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 1fdb08a185..a9a25c5acc 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 @@ -2,6 +2,7 @@ package com.tangem.tap.common.analytics.handlers.amplitude import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsUserIdHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( @@ -9,7 +10,6 @@ class AmplitudeAnalyticsHandler( ) : AnalyticsHandler, AnalyticsUserIdHandler { override fun id(): String = ID - override fun setUserId(userId: String) { client.setUserId(userId) } @@ -18,8 +18,8 @@ class AmplitudeAnalyticsHandler( client.clearUserId() } - override fun send(eventId: String, params: Map) { - client.logEvent(eventId, params) + override fun send(event: AnalyticsEvent) { + client.logEvent(event.id, event.params) } companion object { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt index aa2b3b40db..755f393182 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt @@ -3,8 +3,10 @@ package com.tangem.tap.common.analytics.handlers.appsflyer import android.content.Context import com.appsflyer.AppsFlyerLib import com.tangem.core.analytics.api.EventLogger +import com.tangem.core.analytics.api.UserIdHolder +import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter -interface AppsFlyerAnalyticsClient : EventLogger +interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder internal class AppsFlyerClient( private val context: Context, @@ -13,6 +15,7 @@ internal class AppsFlyerClient( ) : AppsFlyerAnalyticsClient { private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() + private val eventConverter = UnderscoreAnalyticsEventConverter() init { appsFlyerLib.init(key, null, context) @@ -20,7 +23,19 @@ internal class AppsFlyerClient( appsFlyerLib.start(context) } + override fun setUserId(userId: String) { + appsFlyerLib.setCustomerUserId(userId) + } + + override fun clearUserId() { + appsFlyerLib.setCustomerUserId(null) + } + override fun logEvent(event: String, params: Map) { - appsFlyerLib.logEvent(context, event, params) + appsFlyerLib.logEvent( + context, + event, + eventConverter.convertEventParams(params), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt index bd7036b570..d9f80b3476 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt @@ -1,21 +1,38 @@ package com.tangem.tap.common.analytics.handlers.appsflyer import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.AppsFlyerOnlyEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, -) : AnalyticsHandler { +) : AnalyticsHandler, AnalyticsUserIdHandler { override fun id(): String = ID - override fun send(eventId: String, params: Map) { - client.logEvent(eventId, params) + override fun send(event: AnalyticsEvent) { + when (event) { + is AppsFlyerOnlyEvent -> { + client.logEvent(event.id, event.params) + } + is AppsFlyerIncludedEvent -> { + client.logEvent( + event = AnalyticsEvent(category = event.category, event = event.appsFlyerReplacedEvent).id, + params = event.params, + ) + } + } } - override fun send(event: AnalyticsEvent) { - super.send(event) + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() } companion object { @@ -23,12 +40,10 @@ class AppsFlyerAnalyticsHandler( } class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = null - // disabled for now until analytics strategy is defined - // when { - // !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerApiKey, data.config.appsAppId) - // data.isDebug && data.logConfig.appsflyer -> AppsFlyerLogClient(data.jsonConverter) - // else -> null - // }?.let { AppsFlyerAnalyticsHandler(it) } + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when { + !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerApiKey, data.config.appsAppId) + data.isDebug && data.logConfig.isAppsflyerLogEnabled -> AppsFlyerLogClient(data.jsonConverter) + else -> null + }?.let { AppsFlyerAnalyticsHandler(it) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerLogClient.kt index 36284bd8df..a953e00897 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerLogClient.kt @@ -12,4 +12,12 @@ internal class AppsFlyerLogClient( override fun logEvent(event: String, params: Map) { logger.logEvent(event, params) } + + override fun setUserId(userId: String) { + // No-op + } + + override fun clearUserId() { + // No-op + } } \ No newline at end of file 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 7e7106ebb4..4d662a7828 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 @@ -1,8 +1,8 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.tangem.core.analytics.api.AnalyticsErrorHandler -import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent @@ -25,8 +25,8 @@ class FirebaseAnalyticsHandler( client.clearUserId() } - override fun send(eventId: String, params: Map) { - client.logEvent(eventId, params) + override fun send(event: AnalyticsEvent) { + client.logEvent(event.id, event.params) } override fun sendException(event: ExceptionAnalyticsEvent) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 6268ebf230..d982f6e7ae 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -20,7 +20,7 @@ internal class FirebaseClient : FirebaseAnalyticsClient { private val fbAnalytics = Firebase.analytics private val fbCrashlytics = Firebase.crashlytics - private val eventConverter = FirebaseAnalyticsEventConverter() + private val eventConverter = UnderscoreAnalyticsEventConverter() override fun setUserId(userId: String) { Firebase.analytics.setUserId(userId) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/UnderscoreAnalyticsEventConverter.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt rename to app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/UnderscoreAnalyticsEventConverter.kt index 559fc3418b..8d4e3d1261 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/UnderscoreAnalyticsEventConverter.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.handlers.firebase -internal class FirebaseAnalyticsEventConverter { +internal class UnderscoreAnalyticsEventConverter { fun convertEventName(event: String): String { return convertString(event, FIREBASE_EVENT_NAME_MAX_LENGTH) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AppsFlyerOnlyEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AppsFlyerOnlyEvent.kt new file mode 100644 index 0000000000..a265fcfd9f --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AppsFlyerOnlyEvent.kt @@ -0,0 +1,15 @@ +package com.tangem.core.analytics.models + +/** + * Marker interface for AppsFlyer events + * Only events implementing this interface will be sent to AppsFlyer + */ +interface AppsFlyerOnlyEvent + +/** + * Marker interface for AppsFlyer included events + * Events implementing this interface will be sent to AppsFlyer along with other analytics handlers + */ +interface AppsFlyerIncludedEvent { + val appsFlyerReplacedEvent: String +} \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt index f4bc6d2ca5..c1cad4c1a7 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt @@ -2,6 +2,8 @@ package com.tangem.core.analytics.models.event import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.AppsFlyerOnlyEvent sealed class OnboardingAnalyticsEvent( category: String, @@ -14,6 +16,8 @@ sealed class OnboardingAnalyticsEvent( params: Map = mapOf(), ) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) { + class AppsFlyerOnlyEntryScreenView : Onboarding(event = "wallet_entry_screen_view"), AppsFlyerOnlyEvent + class Started( source: String, ) : Onboarding( @@ -64,7 +68,12 @@ sealed class OnboardingAnalyticsEvent( put("Seed Phrase Length", seedPhraseLength.toString()) } }, - ) + ), AppsFlyerIncludedEvent { + override val appsFlyerReplacedEvent = when (creationType) { + WalletCreationType.NewSeed -> "wallet_created_successfully" + WalletCreationType.SeedImport -> "wallet_imported" + } + } sealed class WalletCreationType(val value: String) { data object NewSeed : WalletCreationType(value = "New Seed") @@ -85,6 +94,7 @@ sealed class OnboardingAnalyticsEvent( AnalyticsParam.SOURCE to source, ), ) + class ButtonImportWallet : SeedPhrase("Button - Import Wallet") class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened") class ButtonImport : SeedPhrase("Button - Import") diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 08e59781a0..868a56d99c 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -27,11 +27,7 @@ interface AnalyticsHandler : AnalyticsEventHandler { fun id(): String - fun send(eventId: String, params: Map = emptyMap()) - - override fun send(event: AnalyticsEvent) { - send(event.id, event.params) - } + override fun send(event: AnalyticsEvent) } interface AnalyticsHandlerHolder { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/filter/AppsFlyerEventFilter.kt b/core/analytics/src/main/java/com/tangem/core/analytics/filter/AppsFlyerEventFilter.kt new file mode 100644 index 0000000000..d316469bb1 --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/filter/AppsFlyerEventFilter.kt @@ -0,0 +1,23 @@ +package com.tangem.core.analytics.filter + +import com.tangem.core.analytics.api.AnalyticsEventFilter +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.AppsFlyerOnlyEvent + +class AppsFlyerEventFilter : AnalyticsEventFilter { + + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = + event is AppsFlyerOnlyEvent || event is AppsFlyerIncludedEvent + + override suspend fun canBeSent(event: AnalyticsEvent): Boolean = true + + override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean { + return when (event) { + is AppsFlyerOnlyEvent -> handler.id() == "AppsFlyer" + is AppsFlyerIncludedEvent -> true + else -> false + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 0f188dfe43..1d9409b60b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -60,9 +60,8 @@ internal class CreateMobileWalletModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() - analyticsEventHandler.send( - event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source), - ) + analyticsEventHandler.send(event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source)) + analyticsEventHandler.send(event = OnboardingAnalyticsEvent.Onboarding.AppsFlyerOnlyEntryScreenView()) analyticsEventHandler.send( event = OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened(source = params.source), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 1aa8695bab..101eb5ff04 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent sealed class OnboardingEvent( category: String, @@ -33,7 +34,12 @@ sealed class OnboardingEvent( put("Seed Phrase Length", seedPhraseLength.toString()) } }, - ) + ), AppsFlyerIncludedEvent { + override val appsFlyerReplacedEvent = when (creationType) { + WalletCreationType.NewSeed, WalletCreationType.PrivateKey -> "wallet_created_successfully" + WalletCreationType.SeedImport -> "wallet_imported" + } + } sealed class WalletCreationType(val value: String) { data object PrivateKey : WalletCreationType(value = "Private Key") From 98fa3b5ce5b437fd35120c62b43b782ca488a72c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 17:06:07 +0500 Subject: [PATCH 109/130] Updated on 2026-08-14 --- .../markets/tokenlist/impl/model/MarketsListModel.kt | 4 ++-- .../features/markets/tokenlist/impl/ui/MarketsList.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index e214f30658..27fd01e1d5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -116,7 +116,7 @@ internal class MarketsListModel @Inject constructor( flow4 = shouldShowYieldModeMarketPromoUseCase( appCurrency = currentAppCurrency.value, interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), - ), + ).conflate(), flow5 = getUserCountryUseCase.invoke(), ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo, userCountry -> MarketsItemsData( @@ -134,7 +134,7 @@ internal class MarketsListModel @Inject constructor( flow3 = shouldShowYieldModeMarketPromoUseCase( appCurrency = currentAppCurrency.value, interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), - ), + ).conflate(), flow4 = getUserCountryUseCase.invoke(), ) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo, userCountry -> MarketsItemsData( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 0930d3c1e4..29c313fd89 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -176,14 +176,14 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif ) } - val marketsNotification = state.marketsNotificationUM AnimatedVisibility( - state.isInSearchMode.not() && - state.selectedSortBy != SortByTypeUM.YieldSupply, + state.list !is ListUM.LoadingError && + state.isInSearchMode.not() && state.selectedSortBy != SortByTypeUM.YieldSupply, ) { + val wrappedNotification = remember(this) { state.marketsNotificationUM } val showMore = stringResourceSafe(R.string.common_show_more) - when (marketsNotification) { + when (wrappedNotification) { is MarketsNotificationUM.YieldSupplyPromo -> { val description = stringResourceSafe( R.string.markets_yield_supply_banner_description, @@ -199,7 +199,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif } YieldSupplyInMarketsPromoNotification( - config = marketsNotification.config.copy( + config = wrappedNotification.config.copy( subtitle = clickableDescription, ), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), From 5bb97e290d094e940fd42743248eaac6bde90622 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 17:06:36 +0500 Subject: [PATCH 110/130] Updated on 2026-08-14 --- .../impl/model/TokenActionsHandler.kt | 44 +++++++++++++------ .../portfolio/impl/ui/state/QuickActionUM.kt | 2 +- .../impl/ui/state/TokenActionsBSContentUM.kt | 2 +- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 7561518e9f..854f6ccdcb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -20,6 +20,8 @@ import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.details.NavigationAction +import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -41,6 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val shareManager: ShareManager, + private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, ) { private val disabledActionsInDemoMode = buildSet { @@ -186,22 +189,37 @@ internal class TokenActionsHandler @AssistedInject constructor( val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> currencyData.userWallet.walletId to currencyData.status } - if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) { - router.push( - AppRoute.YieldSupplyActive( + val tokenEnterStatus = yieldSupplyEnterStatusUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() + val isActiveYield = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true + + when { + tokenEnterStatus != null -> router.push( + AppRoute.CurrencyDetails( userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - apy = yieldSupplyApy, - ), - ) - } else { - router.push( - AppRoute.YieldSupplyPromo( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - apy = yieldSupplyApy, + currency = cryptoCurrencyStatus.currency, + navigationAction = NavigationAction.YieldSupply( + isActive = isActiveYield, + ), ), ) + isActiveYield -> { + router.push( + AppRoute.YieldSupplyActive( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } + else -> { + router.push( + AppRoute.YieldSupplyPromo( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt index e0987c16e7..777dd019c1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -44,7 +44,7 @@ internal sealed class QuickActionUM( data class YieldMode( private val apy: String, ) : QuickActionUM( - title = resourceReference(R.string.yield_module_start_earning), + title = resourceReference(R.string.common_yield_mode), description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), icon = R.drawable.ic_analytics_up_mini_24, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 078c25562f..20db6ec796 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -48,7 +48,7 @@ internal data class TokenActionsBSContentUM( iconRes = R.drawable.ic_staking_24, ), YieldMode( - text = resourceReference(R.string.yield_module_start_earning), + text = resourceReference(R.string.common_yield_mode), iconRes = R.drawable.ic_analytics_up_mini_24, ), ; From 737bb507d902f0faa0020357851b14f6f06a0ece Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Dec 2025 18:54:08 +0500 Subject: [PATCH 111/130] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 11 ++++ .../tangem/datasource/api/pay/TangemPayApi.kt | 6 ++ .../request/SetTangemPayEnabledRequest.kt | 9 +++ .../response/CheckCustomerWalletResponse.kt | 1 + .../datasource/local/visa/TangemPayStorage.kt | 3 + core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values-es/strings.xml | 2 + core/res/src/main/res/values-ja/strings.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 4 +- .../message/MessageBottomSheetUMV2.kt | 8 +++ .../message/MessageBottomSheetV2.kt | 57 +++++++++++++++++- .../repository/DefaultOnboardingRepository.kt | 21 ++++++- .../tokens/GetWalletTotalBalanceUseCase.kt | 4 +- .../pay/repository/OnboardingRepository.kt | 2 + .../model/intents/TangemPayClickIntents.kt | 60 +++++++++++++++++++ .../TangemPayUpdateInfoStateTransformer.kt | 51 +++++++++++----- .../state/util/TangemPayStateCreator.kt | 33 ---------- .../subscribers/TangemPayMainSubscriber.kt | 17 +----- 19 files changed, 221 insertions(+), 75 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetTangemPayEnabledRequest.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 80e06699c1..a6f6497a39 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -53,6 +53,12 @@ internal class DefaultTangemPayStorage @Inject constructor( } } + override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) { + withContext(dispatcherProvider.io) { + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") + } + } + override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) @@ -70,6 +76,10 @@ internal class DefaultTangemPayStorage @Inject constructor( ?.let(tokensAdapter::fromJson) } + override suspend fun clearAuthTokens(customerWalletAddress: String) { + secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + } + override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) { withContext(dispatcherProvider.io) { appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) @@ -112,6 +122,7 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = withContext(dispatcherProvider.io) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 07719ff953..1054ffef58 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -29,6 +29,12 @@ interface TangemPayApi { @Path("customer_wallet_id") customerWalletId: String, ): ApiResponse + @PATCH("v1/customer/pay-enabled") + suspend fun setTangemPayEnabledStatus( + @Header("Authorization") authHeader: String, + @Body body: SetTangemPayEnabledRequest, + ): ApiResponse + @POST("v1/deeplink/validate") suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetTangemPayEnabledRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetTangemPayEnabledRequest.kt new file mode 100644 index 0000000000..c159a2418b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetTangemPayEnabledRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SetTangemPayEnabledRequest( + @Json(name = "is_tangem_pay_enabled") val isTangemPayEnabled: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt index 01ee25582c..0a522486f5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -9,5 +9,6 @@ data class CheckCustomerWalletResponse( ) { data class Result( @Json(name = "id") val id: String?, + @Json(name = "is_tangem_pay_enabled") val isTangemPayEnabled: Boolean?, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index a9279e9193..a048c54414 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -8,9 +8,12 @@ interface TangemPayStorage { suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? + suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) + suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? + suspend fun clearAuthTokens(customerWalletAddress: String) suspend fun storeOrderId(customerWalletAddress: String, orderId: String) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index af3e4fd8d7..be2b45ae50 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1072,6 +1072,8 @@ Bitte setze das nächste Gerät zurück, um fortzufahren. Ringbesitzer erhalten bis zum 15.11. 3 provisionsfreie Swaps auf Changelly! Jetzt mit 0 % Gebühren tauschen! + Geräte mit Root-Zugriff gelten als weniger sicher. Deine Daten können zusätzlichen Risiken ausgesetzt sein. + Root-Zugriff erkannt Melde dich bei der App an und überprüfe dein Guthaben, ohne die Karte oder Ring zu scannen Zugriff auf die App Nutzung biometrischer Daten zulassen @@ -1990,6 +1992,7 @@ Gebührenpolitik Tangem erhebt außerdem eine Servicegebühr von 15% auf den erzielten Ertrag. Deine Gelder werden automatisch an Aave überwiesen, sobald die Netzwerkgebühren niedriger sind oder Dein Guthaben den erforderlichen Mindestbetrag erreicht. + Die Gebühren sind aufgrund der hohen Marktaktivität derzeit höher als üblich. Du kannst jetzt fortfahren oder später noch einmal vorbeischauen, wenn die Gebühren niedriger sind. Hohe Netzwerkgebühren Historische Renditen Aktiviere %1$s%% Jahreszins auf Dein Guthaben diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index e99920eeb5..92108a3ab5 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -916,6 +916,8 @@ El reseteo a valores de fábrica eliminará completamente la billetera de la tarjeta/anillo seleccionado y lo eliminará de la app. No podrá restaurar la billetera actual. Si tiene un Anillo Tangem, ¡3 swaps sin comisión en Changelly hasta el 15/11! ¡Intercambia con 0% de comisión! + Los dispositivos con jailbreak se consideran menos seguros. Sus datos podrían estar expuestos a riesgos adicionales. + Acceso root detectado Inicie sesión en la app y comprueba su saldo sin escanear la tarjeta o el anillo Acceder a la app Permitir el uso de biometría diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 70f1b95349..dbc266076b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1617,7 +1617,7 @@ ロック解除 カードをスキャンしてアクセスロックを解除する ロック解除が必要 - ウォレットの追加方法を選択してください + ウォレットの種類を選択します Tangemカードまたはリングをスキャンして復元するか、別のウォレットからインポートしてください。 ハードウェアウォレットを作成 Tangemウォレットを購入しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 036b504b45..537e6d8e15 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1634,7 +1634,7 @@ ПИН не принят. Попробуйте ещё раз или введите другой код. Слабый ПИН: не используйте повторы или последовательности. Разблокировать - Выберите способ добавления кошелька + Выберите тип кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. Создать аппаратный кошелёк Хотите приобрести кошелек Tangem? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index eaddf5b46e..2598e6fe4b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1501,6 +1501,8 @@ Issuing your card We’re getting your card ready. This may take a little time. Tangem Pay + Confirm Cancellation + Are you sure you want to stop the KYC process? You can return to it anytime. We could not verify your profile. If you have any questions, please contact support. Unfortunately, we couldn\'t verify your identity KYC in progress @@ -1686,7 +1688,7 @@ Unlock Scan your card to unlock access Needed unlock - Choose how to add your wallet + Choose your wallet type Scan your Tangem card or ring to restore it or import from another wallet. Create Hardware Wallet Want to purchase a Tangem Wallet? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUMV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUMV2.kt index 0fc219bba4..a776ddd445 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUMV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUMV2.kt @@ -41,6 +41,9 @@ data class MessageBottomSheetUMV2( } } + @Immutable + data class IconImage(@DrawableRes internal var res: Int) : Element + @Immutable data class Chip( internal var text: TextReference, @@ -54,6 +57,7 @@ data class MessageBottomSheetUMV2( @Immutable data class InfoBlock( internal var icon: Icon? = null, + internal var iconImage: IconImage? = null, internal var chip: Chip? = null, var title: TextReference? = null, var body: TextReference? = null, @@ -106,6 +110,10 @@ fun MessageBottomSheetUMV2.InfoBlock.icon(@DrawableRes res: Int, init: MessageBo icon = MessageBottomSheetUMV2.Icon(res).apply(init) } +fun MessageBottomSheetUMV2.InfoBlock.iconImage(@DrawableRes res: Int) = apply { + iconImage = MessageBottomSheetUMV2.IconImage(res) +} + fun MessageBottomSheetUMV2.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUMV2.Chip.() -> Unit = {}) = apply { chip = MessageBottomSheetUMV2.Chip(text).apply(init) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index cb3f2c538d..7b3ab2dd0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -1,14 +1,18 @@ package com.tangem.core.ui.components.bottomsheets.message +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -88,9 +92,7 @@ fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifie @Composable private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier: Modifier = Modifier) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { - state.icon?.let { - BottomSheetIcon(it) - } + BottomSheetIconContainer(state.icon, state.iconImage) state.title?.let { title -> Text( modifier = Modifier @@ -122,6 +124,26 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier: } } +@Suppress("CanBeNonNullable") +@Composable +private fun BottomSheetIconContainer( + icon: MessageBottomSheetUMV2.Icon?, + iconImage: MessageBottomSheetUMV2.IconImage?, + modifier: Modifier = Modifier, +) { + if (icon != null) { + BottomSheetIcon(icon, modifier) + } else if (iconImage != null) { + Image( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape), + painter = painterResource(id = iconImage.res), + contentDescription = null, + ) + } +} + @Composable private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifier = Modifier) { val tint = when (icon.type) { @@ -236,4 +258,33 @@ private fun Preview() { onDismissRequest = {}, ) } +} + +@Preview +@Composable +private fun Preview2() { + TangemThemePreview { + MessageBottomSheetV2( + messageBottomSheetUM { + infoBlock { + iconImage = MessageBottomSheetUMV2.IconImage(R.drawable.img_visa_notification) + title = TextReference.Str("Title Title Title") + body = TextReference.Str("Body") + chip(text = TextReference.Str("Some chip information")) + } + primaryButton { + text = TextReference.Str("Test") + icon = R.drawable.ic_tangem_24 + } + secondaryButton { + icon = R.drawable.ic_tangem_24 + text = TextReference.Str("asdasd") + onClick { + closeBs() + } + } + }, + onDismissRequest = {}, + ) + } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index e11aa1c09b..79fed1dc36 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest +import com.tangem.datasource.api.pay.models.request.SetTangemPayEnabledRequest import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage @@ -180,9 +181,10 @@ internal class DefaultOnboardingRepository @Inject constructor( ) }.map { response -> val id = response.result?.id - val isPaeraCustomer = !id.isNullOrEmpty() - tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, isPaeraCustomer) - isPaeraCustomer + val isTangemPayEnabled = response.result?.isTangemPayEnabled == true + val shouldShowTangemPayBlock = !id.isNullOrEmpty() && isTangemPayEnabled + tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, shouldShowTangemPayBlock) + shouldShowTangemPayBlock }.mapLeft { error -> if (error is VisaApiError.NotPaeraCustomer) { tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, false) @@ -205,4 +207,17 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) { tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true) } + + override suspend fun disableTangemPay(userWalletId: UserWalletId): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.setTangemPayEnabledStatus( + authHeader = authHeader, + body = SetTangemPayEnabledRequest(isTangemPayEnabled = false), + ) + }.onRight { + val address = requestHelper.getCustomerWalletAddress(userWalletId) + tangemPayStorage.clearAll(userWalletId = userWalletId, customerWalletAddress = address) + setHideMainOnboardingBanner(userWalletId) + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 18401f9621..11760b5e2f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -26,9 +26,9 @@ class GetWalletTotalBalanceUseCase( private val walletBalanceCache = ConcurrentHashMap() operator fun invoke( - userTallestIds: Collection, + userWalletsIds: Collection, ): LceFlow> { - val flows = userTallestIds.distinct() + val flows = userWalletsIds.distinct() .map { userWalletId -> invoke(userWalletId).map { maybeBalance -> userWalletId to maybeBalance diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index e3fc78e653..6a304dbfbb 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -32,4 +32,6 @@ interface OnboardingRepository { suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) + + suspend fun disableTangemPay(userWalletId: UserWalletId): Either } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index b00b4b5886..7fc89a52ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -6,11 +6,14 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -26,6 +29,10 @@ internal interface TangemPayIntents { fun onRefreshPayToken(userWalletId: UserWalletId) + fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) + + fun onKycProgressClicked(userWalletId: UserWalletId) + fun onIssuingCardClicked() fun onIssuingFailedClicked() @@ -47,6 +54,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayOnboardingRepository: OnboardingRepository, private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), TangemPayIntents { @@ -67,6 +75,51 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( } } + override fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) { + router.openTangemPayDetails( + userWalletId = userWalletId, + config = config, + ) + } + + override fun onKycProgressClicked(userWalletId: UserWalletId) { + val cancelKycConfirmDialogMessage = DialogMessage( + title = resourceReference(R.string.tangempay_kyc_confirm_cancellation_alert_title), + message = resourceReference(R.string.tangempay_kyc_confirm_cancellation_description), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_not_now), + onClick = { }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.common_confirm), + onClick = { disableTangemPay(userWalletId) }, + ), + ) + val kycInfoBottomSheet = bottomSheetMessage { + infoBlock { + iconImage(res = com.tangem.core.ui.R.drawable.img_visa_notification) + title = resourceReference(R.string.tangempay_kyc_in_progress) + body = resourceReference(R.string.tangempay_kyc_in_progress_popup_description) + } + primaryButton { + text = resourceReference(R.string.tangempay_kyc_in_progress_notification_button) + onClick = { + router.openTangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId)) + closeBs() + } + } + secondaryButton { + text = resourceReference(R.string.tangempay_cancel_kyc) + onClick = { + uiMessageSender.send(cancelKycConfirmDialogMessage) + closeBs() + } + } + } + + uiMessageSender.send(kycInfoBottomSheet) + } + override fun onIssuingCardClicked() { val issuingBottomSheet = bottomSheetMessage { infoBlock { @@ -132,4 +185,11 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( onboardingRepository.setHideMainOnboardingBanner(userWalletId) } } + + private fun disableTangemPay(userWalletId: UserWalletId) { + modelScope.launch { + tangemPayOnboardingRepository.disableTangemPay(userWalletId) + .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index a47a0aec3a..b6ad2482ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat @@ -11,11 +12,10 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createCancelledState -import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState -import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState import java.util.Currency /** @@ -26,12 +26,9 @@ private const val POLYGON_CHAIN_ID = 137 internal class TangemPayUpdateInfoStateTransformer( userWalletId: UserWalletId, - private val value: MainScreenCustomerInfo? = null, + private val value: MainScreenCustomerInfo, private val cardFrozenState: TangemPayCardFrozenState, - private val onClickKyc: () -> Unit = {}, - private val onIssuingCard: () -> Unit = {}, - private val onIssuingFailed: () -> Unit = {}, - private val openDetails: (config: TangemPayDetailsConfig) -> Unit = {}, + private val tangemPayClickIntents: TangemPayIntents, ) : WalletStateTransformer(userWalletId = userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -44,16 +41,15 @@ internal class TangemPayUpdateInfoStateTransformer( } private fun createInitialState(): TangemPayState { - val cardInfo = value?.info?.cardInfo - val productInstance = value?.info?.productInstance + val cardInfo = value.info.cardInfo + val productInstance = value.info.productInstance // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { - value == null -> TangemPayState.Empty - value.orderStatus == OrderStatus.CANCELED -> createCancelledState(onIssuingFailed) - !value.info.isKycApproved -> createKycInProgressState(onClickKyc) + value.orderStatus == OrderStatus.CANCELED -> createCancelledState() + !value.info.isKycApproved -> createKycInProgressState() cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance) - else -> createIssueProgressState(onIssuingCard) + else -> createIssueProgressState() } } @@ -63,7 +59,8 @@ internal class TangemPayUpdateInfoStateTransformer( balanceText = TextReference.Str(getBalanceText(cardInfo)), balanceSymbol = stringReference("USDC"), // TODO hardcode for now onClick = { - openDetails( + tangemPayClickIntents.openDetails( + userWalletId, TangemPayDetailsConfig( cardId = productInstance.cardId, isPinSet = cardInfo.isPinSet, @@ -82,4 +79,28 @@ internal class TangemPayUpdateInfoStateTransformer( fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) } } + + private fun createKycInProgressState(): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_kyc_in_progress), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = { tangemPayClickIntents.onKycProgressClicked(userWalletId) }, + ) + + private fun createIssueProgressState(): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_issuing_your_card), + buttonText = TextReference.EMPTY, + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = tangemPayClickIntents::onIssuingCardClicked, + showProgress = true, + ) + + private fun createCancelledState(): TangemPayState = TangemPayState.FailedIssue( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_failed_to_issue_card), + iconRes = R.drawable.ic_alert_24, + onButtonClick = tangemPayClickIntents::onIssuingFailedClicked, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt deleted file mode 100644 index 51b584d38f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.util - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState -import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress - -internal object TangemPayStateCreator { - - fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_payment_account), - description = TextReference.Res(R.string.tangempay_kyc_in_progress), - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = onClickKyc, - ) - - fun createIssueProgressState(onIssuingCardClick: () -> Unit): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_payment_account), - description = TextReference.Res(R.string.tangempay_issuing_your_card), - buttonText = TextReference.EMPTY, - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = onIssuingCardClick, - showProgress = true, - ) - - fun createCancelledState(onIssueFailedClick: () -> Unit): TangemPayState = TangemPayState.FailedIssue( - title = TextReference.Res(R.string.tangempay_payment_account), - description = TextReference.Res(R.string.tangempay_failed_to_issue_card), - iconRes = R.drawable.ic_alert_24, - onButtonClick = onIssueFailedClick, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 6a7cf0a950..8b890929a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import com.tangem.common.routing.AppRoute import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.MainCustomerInfoContentState @@ -10,7 +9,6 @@ import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.* @@ -28,7 +26,6 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, - private val innerWalletRouter: InnerWalletRouter, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val analytics: WalletTangemPayAnalyticsEventSender, @@ -102,19 +99,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( userWalletId = userWalletId, value = data, cardFrozenState = cardFrozenState, - onClickKyc = { - innerWalletRouter.openTangemPayOnboarding( - mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId), - ) - }, - onIssuingCard = clickIntents::onIssuingCardClicked, - onIssuingFailed = clickIntents::onIssuingFailedClicked, - openDetails = { config -> - innerWalletRouter.openTangemPayDetails( - userWalletId = userWalletId, - config = config, - ) - }, + tangemPayClickIntents = clickIntents, ), ) } From f9d1e222afc589f981b982b2cae6c329ca766dd2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 15:14:53 +0500 Subject: [PATCH 112/130] Updated on 2026-08-14 --- .../data/yield/supply/DefaultYieldSupplyRepository.kt | 10 +++++----- .../features/markets/tokenlist/impl/ui/MarketsList.kt | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index a40499ba87..d7faa6176a 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -27,6 +27,7 @@ import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -155,7 +156,7 @@ internal class DefaultYieldSupplyRepository( override suspend fun getTokenPendingStatus( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): YieldSupplyEnterStatus? = try { + ): YieldSupplyEnterStatus? = runSuspendCatching { val cryptoCurrency = cryptoCurrencyStatus.currency val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -174,10 +175,9 @@ internal class DefaultYieldSupplyRepository( hasRecentYieldExitTxs -> YieldSupplyEnterStatus.Exit else -> null } - } catch (e: Exception) { - Timber.e(e, "Failed to get pending yield supply status") - null - } + }.onFailure { exception -> + Timber.w(exception, "Failed to get pending yield supply status") + }.getOrNull() override fun getShouldShowYieldPromoBanner(): Flow { return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 29c313fd89..0a7e31ebfc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -176,14 +176,14 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif ) } + val marketsNotificationUM = state.marketsNotificationUM AnimatedVisibility( state.list !is ListUM.LoadingError && state.isInSearchMode.not() && state.selectedSortBy != SortByTypeUM.YieldSupply, ) { - val wrappedNotification = remember(this) { state.marketsNotificationUM } val showMore = stringResourceSafe(R.string.common_show_more) - when (wrappedNotification) { + when (marketsNotificationUM) { is MarketsNotificationUM.YieldSupplyPromo -> { val description = stringResourceSafe( R.string.markets_yield_supply_banner_description, @@ -199,7 +199,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif } YieldSupplyInMarketsPromoNotification( - config = wrappedNotification.config.copy( + config = marketsNotificationUM.config.copy( subtitle = clickableDescription, ), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), From c9a7c85a896fb712c12218cef0bcd635049d42a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 11:57:55 +0500 Subject: [PATCH 113/130] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 1 - core/res/src/main/res/values/strings.xml | 1 - .../kotlin/com/tangem/features/details/utils/ItemsBuilder.kt | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index dbc266076b..5ba1fbe88a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -422,7 +422,6 @@ トークンは誰でも作成できることに注意してください。 Tangemウォレットを購入 チャット - Tangem Visaを入手 アクセスコード カードをスキャンする前に、正しいアクセスコードを送信する必要があります。 長くタップ diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2598e6fe4b..76cee235d8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -430,7 +430,6 @@ Note that tokens can be created by anyone Buy Tangem Wallet Chat - Get Tangem Visa Access code You will have to submit the correct access code before scanning the card Long Tap diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 0cdd6b6a10..8de88bbb26 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -129,7 +129,7 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item( id = "get_tangem_visa", block = BlockUM( - text = resourceReference(R.string.details_get_visa), + text = resourceReference(R.string.tangempay_get_tangem_pay), iconRes = R.drawable.ic_tangem_pay_24, onClick = { router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) From bbc732b01fd22eaf7f094bf130aa149072a5cc34 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 12:29:33 +0500 Subject: [PATCH 114/130] Updated on 2026-08-14 --- .../tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index d4c6dc4800..3f416e65e0 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -126,7 +126,8 @@ class VisaCustomerWalletApproveTask( extendedPublicKey = extendedPublicKey, ) - visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress) + val signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress) + callback(CompletionResult.Success(signedData)) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) From 1037d2ff103a1be2d85413284387bb7596e66c42 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 12:01:22 +0500 Subject: [PATCH 115/130] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 34 +++++++++++++------ .../local/preferences/PreferencesKeys.kt | 1 + .../datasource/local/visa/TangemPayStorage.kt | 3 ++ .../pay/DefaultTangemPayEligibilityManager.kt | 10 +++++- .../repository/DefaultOnboardingRepository.kt | 10 +++++- .../domain/pay/TangemPayEligibilityManager.kt | 1 + .../pay/repository/OnboardingRepository.kt | 1 + .../features/details/model/DetailsModel.kt | 17 ++++++++-- .../features/details/utils/ItemsBuilder.kt | 24 +++++++++---- 9 files changed, 79 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index a6f6497a39..fb02cff093 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage @@ -21,6 +22,7 @@ import javax.inject.Singleton private const val AUTH_TOKENS_DEFAULT_KEY = "tangem_pay_default_key" private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key" +@Suppress("TooManyFunctions") @Singleton internal class DefaultTangemPayStorage @Inject constructor( @ApplicationContext applicationContext: Context, @@ -119,16 +121,6 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = - withContext(dispatcherProvider.io) { - secureStorage.delete(createAuthTokensKey(customerWalletAddress)) - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) - } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) @@ -170,6 +162,28 @@ internal class DefaultTangemPayStorage @Inject constructor( } } + override suspend fun storeTangemPayEligibility(eligibility: Boolean) { + withContext(dispatcherProvider.io) { + appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) + } + } + + override suspend fun getTangemPayEligibility(): Boolean { + return withContext(dispatcherProvider.io) { + appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) + } + } + + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + withContext(dispatcherProvider.io) { + secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) + } + private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId" diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 78e99e42bd..544a55357f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -154,6 +154,7 @@ object PreferencesKeys { } val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") } + val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") } fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") // endregion diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index a048c54414..591bc9f6be 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.local.visa import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.visa.model.TangemPayAuthTokens +@Suppress("TooManyFunctions") interface TangemPayStorage { suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) @@ -36,6 +37,8 @@ interface TangemPayStorage { suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) + suspend fun storeTangemPayEligibility(eligibility: Boolean) + suspend fun getTangemPayEligibility(): Boolean suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index f2d850f463..4ab4d074f2 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -45,6 +45,10 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } + override suspend fun getTangemPayAvailability(): Boolean { + return onboardingRepository.checkCustomerEligibility() + } + private suspend fun getUserWalletsData(): List { cachedEligibleWallets?.let { return it } @@ -69,7 +73,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } private suspend fun getPossibleWalletsForTangemPay(): List { - if (!onboardingRepository.checkCustomerEligibility()) { + if (!checkTangemPayEligibility()) { return emptyList() } @@ -125,6 +129,10 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( eligibleWalletsDeferred = null } + private suspend fun checkTangemPayEligibility(): Boolean { + return onboardingRepository.getCustomerEligibility() || onboardingRepository.checkCustomerEligibility() + } + private data class UserWalletData( val userWallet: UserWallet, val isPaeraCustomer: Boolean, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 79fed1dc36..fe06e7b5e3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -197,7 +197,15 @@ internal class DefaultOnboardingRepository @Inject constructor( val response = requestHelper.performWithoutToken { tangemPayApi.checkCustomerEligibility() }.getOrNull() - return response?.result?.isTangemPayAvailable == true + + val isAvailable = response?.result?.isTangemPayAvailable == true + tangemPayStorage.storeTangemPayEligibility(eligibility = isAvailable) + + return isAvailable + } + + override suspend fun getCustomerEligibility(): Boolean { + return tangemPayStorage.getTangemPayEligibility() } override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt index 5ad9a9d3d9..cc26e4832a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -5,4 +5,5 @@ import com.tangem.domain.models.wallet.UserWallet interface TangemPayEligibilityManager { suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List + suspend fun getTangemPayAvailability(): Boolean } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 6a304dbfbb..1deca6b43a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -26,6 +26,7 @@ interface OnboardingRepository { suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either suspend fun checkCustomerEligibility(): Boolean + suspend fun getCustomerEligibility(): Boolean fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? 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 b730125b22..992afae4ac 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 @@ -53,19 +53,19 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class DetailsModel @Inject constructor( socialsBuilder: SocialsBuilder, + paramsContainer: ParamsContainer, + feedbackFeatureToggles: FeedbackFeatureToggles, private val itemsBuilder: ItemsBuilder, private val appVersionProvider: AppVersionProvider, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, private val urlOpener: UrlOpener, private val appInstanceIdProvider: AppInstanceIdProvider, - paramsContainer: ParamsContainer, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val appStateHolder: ReduxStateHolder, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val feedbackFeatureToggles: FeedbackFeatureToggles, override val dispatchers: CoroutineDispatcherProvider, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -254,7 +254,18 @@ internal class DetailsModel @Inject constructor( .getEligibleWallets(shouldExcludePaeraCustomers = true) .isNotEmpty() if (isEligible) { - items.update { itemsBuilder.addVisaItem(it) } + items.update { itemsBuilder.addTangemPayItem(items = it, onClick = ::onTangemPayItemClicked) } + } + } + } + + private fun onTangemPayItemClicked() { + modelScope.launch { + val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() + if (isEligible) { + router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) + } else { + items.update { itemsBuilder.removeTangemPayItem(it) } } } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 8de88bbb26..c8627bf1ea 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -14,6 +14,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import javax.inject.Inject +private const val TANGEM_PAY_ITEM_ID = "get_tangem_pay" + @ModelScoped internal class ItemsBuilder @Inject constructor(private val router: Router) { @@ -37,10 +39,13 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ).let(::add) }.toImmutableList() - fun addVisaItem(items: ImmutableList): ImmutableList { + fun addTangemPayItem(items: ImmutableList, onClick: () -> Unit): ImmutableList { return items.toMutableList().map { block -> if (block.id == "shop" && block is DetailsItemUM.Basic) { - val newItems = block.items.toMutableList().apply { add(getVisaItem()) } + val newItems = block + .items + .toMutableList() + .apply { add(getTangemPayItem(onClick = onClick)) } block.copy(items = newItems.toImmutableList()) } else { block @@ -48,6 +53,13 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { }.toImmutableList() } + fun removeTangemPayItem(items: ImmutableList): ImmutableList { + val tangemPayItem = items.find { it.id == TANGEM_PAY_ITEM_ID } ?: return items + return items.toMutableList() + .also { list -> list.remove(tangemPayItem) } + .toImmutableList() + } + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( @@ -126,14 +138,12 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { }.toPersistentList(), ) - private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item( - id = "get_tangem_visa", + private fun getTangemPayItem(onClick: () -> Unit): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item( + id = TANGEM_PAY_ITEM_ID, block = BlockUM( text = resourceReference(R.string.tangempay_get_tangem_pay), iconRes = R.drawable.ic_tangem_pay_24, - onClick = { - router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) - }, + onClick = onClick, ), ) } \ No newline at end of file From a0432c18bda42efedd497e5b46972ab25f6c48c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 18:22:17 +0500 Subject: [PATCH 116/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 462e99b59c..55284d02b1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1329" +tangemBlockchainSdk = "releases-5.32-1331" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.32-574" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 87fbd38dd717a3222ce0d8d1919e813fbca972ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 17:04:17 +0300 Subject: [PATCH 117/130] Updated on 2026-08-14 --- .../java/com/tangem/data/wallets/DefaultWalletsRepository.kt | 1 + .../tangem/domain/wallets/repository/WalletsRepository.kt | 5 +++-- .../wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt | 1 + .../tangem/feature/wallet/child/wallet/model/WalletModel.kt | 4 +++- 4 files changed, 8 insertions(+), 3 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 797d06f372..45b444d02f 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 @@ -53,6 +53,7 @@ internal class DefaultWalletsRepository( private val upgradeWalletNotificationDisabled: MutableStateFlow> = MutableStateFlow(mutableSetOf()) + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override suspend fun shouldSaveUserWalletsSync(): Boolean { return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } 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 96266b809d..430653e85c 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 @@ -11,12 +11,13 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface WalletsRepository { + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") suspend fun shouldSaveUserWalletsSync(): Boolean - @Deprecated("Hot wallet make always save user wallets. Do not use this method") + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") fun shouldSaveUserWallets(): Flow - @Deprecated("Hot wallet make always save user wallets. Do not use this method") + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") suspend fun saveShouldSaveUserWallets(item: Boolean) suspend fun useBiometricAuthentication(): Boolean diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt index 16efc0eadb..85e7b0265f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.repository.WalletsRepository +@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") class ShouldSaveUserWalletsSyncUseCase(private val walletsRepository: WalletsRepository) { suspend operator fun invoke(): Boolean = walletsRepository.shouldSaveUserWalletsSync() 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 c4b8aeb87c..8370377f18 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 @@ -304,7 +304,9 @@ internal class WalletModel @Inject constructor( notificationsRepository.setShouldAskNotificationPermissionsViaBs(true) return@launch } - if (!isBiometricsEnabled) return@launch + if (!hotWalletFeatureToggles.isHotWalletEnabled && !isBiometricsEnabled) { + return@launch + } if (!shouldShow) { return@launch } From 5540f76392b03682120f05f8532be9fc7534831c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Dec 2025 20:49:32 +0500 Subject: [PATCH 118/130] Updated on 2026-08-14 --- .../features/markets/tokenlist/impl/model/MarketsListModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 27fd01e1d5..d33935747e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager @@ -147,7 +148,8 @@ internal class MarketsListModel @Inject constructor( } } }.collect { marketsItemsData -> - val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo + val isApplyFCARestrictions = marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo && !isApplyFCARestrictions if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) { analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown()) } From 774f25bf4a381bee3672aa880a62e0a0411759d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Dec 2025 17:45:06 +0500 Subject: [PATCH 119/130] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 28 ++++-- core/res/src/main/res/values-es/strings.xml | 88 +++++++++++++++-- core/res/src/main/res/values-fr/strings.xml | 99 +++++++++++++++++-- core/res/src/main/res/values-it/strings.xml | 12 +-- core/res/src/main/res/values-ja/strings.xml | 30 ++++-- core/res/src/main/res/values-ru/strings.xml | 20 ++-- .../src/main/res/values-uk-rUA/strings.xml | 95 +++++++++++++++++- .../src/main/res/values-zh-rTW/strings.xml | 12 +-- core/res/src/main/res/values/strings.xml | 74 ++++++++------ .../wallet/state/model/WalletNotification.kt | 7 +- .../TangemPayRefreshNeededStateTransformer.kt | 7 ++ .../subscribers/TangemPayMainSubscriber.kt | 1 + .../components/visa/TangemPayRefreshBlock.kt | 1 + 13 files changed, 389 insertions(+), 85 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index be2b45ae50..cefd04ae41 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -44,6 +44,7 @@ Speichern Kontoname Der Kontoname ist bereits vorhanden + Der Kontoname wird bereits verwendet. Konto Neues Konto Konto hinzufügen @@ -593,10 +594,13 @@ Aktualisiere Deine aktuelle Wallet Gehe zum Backup Bitte sicher Deine Wallet, bevor Du einen Zugangscode erstellst. + Sicherung zuerst beenden Zuerst die Sicherung abschließen Unvollständig Andere Methoden + Speicher Deinen Wiederherstellungssatz an einem sicheren Ort und halte diesen stets geheim, um Dein Geld zu schützen. Wiederherstellungs-Phrase + Um Deine Wallet mit einem Zugangscode zu sichern, schließe den Sicherungsvorgang ab. Um Deine Wallet auf Hardware umzustellen, erstelle vorher ein Backup. Deine privaten Schlüssel sind sicher verschlüsselt und auf Deinem Telefon gespeichert. Schlüssel werden in der App gespeichert @@ -617,8 +621,9 @@ Für diese Wallet existiert ein Backup. Überprüfe dieses bitte, bevor Du sie entfernst, um sicherzustellen, dass Du sie später wiederherstellen kannst. Wenn Du diese Wallet ohne Backup entfernst, verlierst Du dauerhaft den Zugriff auf Deine Assets. Diese Wallet dauerhaft entfernen? - Mir ist bewusst, dass ich den Zugriff auf meine Wallet verliere kann, wenn ich sie vor der Entfernung nicht gesichert habe. + Mir ist bewusst, dass ich den Zugriff auf meine Wallet verlieren kann, wenn ich sie vor der Entfernung nicht gesichert habe. Mir ist bewusst, dass durch das Entfernen meiner Wallet diese nicht gelöscht, sondern lediglich von meinem Gerät entfernt wird. + Upgrade Es wird keine Seed-Phrase mehr benötigt – Deine Tangem-Karte oder Dein Tangem-Ring wird zu Deinem sicheren Backup. Backup mit Tangem Ein Upgrade ist nicht möglich. Auf diesem Gerät ist bereits eine Wallet vorhanden. @@ -793,6 +798,10 @@ Du musst auf die folgende Version aktualisieren: %1$s um eine mobile Wallet zu erstellen Mobile Wallet erfordert %1$s oder später Alle Neuigkeiten + Gefällt mir + Kurze Zusammenfassung + Verwandte Nachrichten + Quellen Auf dem Laufenden bleiben NFC ist auf deinem Gerät nicht verfügbar Über NFT @@ -1419,6 +1428,7 @@ Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut. Ihre Karte ist entsperrt. Abhebung + KYC abbrechen Guthaben hinzufügen Aufladeoptionen Kartennummer @@ -1446,6 +1456,7 @@ Alles erledigt! Deine Karte ist einsatzbereit. Karte zu Google Pay hinzufügen Karte zu Apple Pay hinzufügen + Pin Code Teile Deine Adresse mit oder zeig den QR-Code. Es wurden technische Probleme festgestellt. Bitte versuche es später erneut oder kontaktiere den Support. Empfangen ist jetzt nicht verfügbar @@ -1454,6 +1465,7 @@ Tausche beliebige Vermögenswerte in Deinem Portfolio gegen eine Karte. Kartendetails Karte entsperren + Dein PIN-Code Auszahlung Auszahlung derzeit nicht möglich Sie können keinen Tausch oder eine neue Auszahlung starten, bis die aktuelle abgeschlossen ist. @@ -1463,7 +1475,7 @@ Kartenausstellung fehlgeschlagen Ein technischer Fehler ist aufgetreten, bitte versuchen Sie es erneut, indem Sie auf die Schaltfläche unten klicken Ein technischer Fehler ist aufgetreten, bitte kontaktieren Sie den Support - Nutzen Sie Ihre Kryptowährungen für Einkäufe im Alltag. \nEine Zahlungskarte, die ihresgleichen sucht. + Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Tangem Pay erhalten Zum Support Es dauert in der Regel bis zu 15 Minuten. @@ -1471,13 +1483,15 @@ Ausstellung Deiner Karte Wir bereiten Ihre Karte vor. Dies kann etwas dauern. Tangem Pay + Konvertierung bestätigen + Möchtest Du den KYC wirklich abbrechen? Du kannst später jederzeit weiter machen. Wir konnten Ihr Profil nicht verifizieren. Bei Fragen wenden Sie sich bitte an den Support. Leider konnten wir Ihre Identität nicht verifizieren KYC in Bearbeitung Status anzeigen KYC für Tangem Pay in Arbeit - Nutzen Sie Ihre Kryptowährungen für Einkäufe im echten Leben. \nEs ist eine Zahlungskarte, die ihresgleichen sucht. - Tangem Visa Card + Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte + Nutzen Sie USDC für alltägliche Zahlungen Karte erhalten Mit digitaler Karte, die mit Apple Pay und Google Pay funktioniert Geben Sie Ihre Vermögenswerte überall aus @@ -1487,15 +1501,15 @@ Unerreichte Privatsphäre Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten Zahlungskonto - Synchronisierung des Zahlungskontos erforderlich + Zahlungskonto ist nicht synchronisiert Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut. Service vorübergehend nicht verfügbar Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. Synchronisation erforderlich - Tangem Visa Card + Nutzen Sie USDC für alltägliche Zahlungen Tangem Pay ist vorübergehend nicht verfügbar Tangem Pay - Verwenden Sie Ihre Karte oder Ihren Ring, um den Zugriff auf Ihr Zahlungskonto wiederherzustellen + Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen Ihr PIN-Code Das ist meine Wallet Guthaben versteckt diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 92108a3ab5..c1cf00a410 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -80,6 +80,7 @@ Seleccione una billetera para iniciar sesión ¡Bienvenido de nuevo! Realizó una copia de seguridad de su billetera correctamente. + Estas palabras son irrecuperables si se pierden. Guárdelas en un lugar seguro. Copia de seguridad completada Su frase secreta de recuperación es un conjunto fijo de %s palabras aleatorias que se utilizan para acceder a su billetera y recuperarla. Estas palabras no pueden recuperarse si se pierden. Asegúrese de guardarlas en un lugar seguro. @@ -87,6 +88,7 @@ Guarde estas %s palabras en un lugar seguro, como un administrador de contraseñas, y nunca las comparta con nadie. No se puede restaurar Frase de recuperación + Nunca comparta estas palabras con nadie. Tangem nunca se las pedirá. Las palabras %s que aparecen a continuación son la frase de recuperación de su billetera. Úselas para restaurar su billetera si pierde su dispositivo. Escriba estas %s palabras en orden y guárdelas en un lugar privado y seguro La responsabilidad total sobre la seguridad y copia de seguridad de la billetera y su frase de recuperación recae en el usuario, no en Tangem. Frase de recuperación @@ -507,6 +509,13 @@ Para continuar, conceda a los contratos inteligentes de %1s permiso para utilizar su %2s Dar autorización Ilimitado + Su copia de seguridad se crea utilizando 2 o 3 tarjetas Tangem. Guárdalas en lugares seguros y separados para protegerlas de pérdidas o daños. + Copia de seguridad con varias tarjetas + Agregar billetera Tangem + Su clave privada se genera directamente dentro de la tarjeta Tangem y nunca sale de ella. + Generación de claves + Todas las operaciones criptográficas ocurren dentro del chip seguro, certificado contra la clonación y la manipulación física. + Seguridad a nivel de hardware Agregar Billetera Existente Crear Nueva Billetera Pedir Tangem @@ -515,7 +524,73 @@ a %s En la red %s ¿Está seguro de que desea salir del proceso de creación de código de acceso? + Hacer copia ahora + Para completar la configuración, haga una copia de seguridad de su billetera y proteja la aplicación con un código de acceso. + Finalizar ahora + Finalizar la configuración de la billetera + Complete la configuración protegiendo la aplicación con un código de acceso. + Si sale, tendrá que empezar de nuevo. + ¿Está seguro de que desea salir del proceso de configuración? + Mantiene sus criptomonedas seguras y sin conexión. Tan delgadas como una tarjeta de crédito, más seguras que una bóveda bancaria. Si lo hace, tendrá que empezar de nuevo. + Recuperar la billetera existente a través de una copia de seguridad de Google Drive + Copia de seguridad de Google Drive + Cree una billetera segura y transfiera sus fondos para mayor protección. + Crear nueva billetera + Mejore su seguridad con la billetera de hardware superior Tangem. + Billetera de hardware + Mueva su billetera actual a Tangem. + Actualizar la billetera actual + Ir a copia de seguridad + Por favor, haga una copia de seguridad de su billetera antes de crear un código de acceso. + Finalice la copia de seguridad primero + Finalizar la copia de seguridad primero + Incompleto + Otros métodos + Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos. + Frase de recuperación + Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad. + Para actualizar a una billetera de hardware, complete el proceso de copia de seguridad. + Sus claves privadas están encriptadas de forma segura y almacenadas en su teléfono + Las claves privadas permanecen en su dispositivo + Cree o restaure su billetera con una frase de recuperación. + Copia de seguridad de la frase semilla + Crear una billetera móvil + Importar billetera existente + Esta frase de recuperación ya ha sido importada + Billetera móvil + Olvidar billetera + Esta billetera se eliminará permanentemente de su dispositivo. + ¿Está seguro que desea hacer esto? + Olvidar billetera + Ir a copia de seguridad + Ver copia de seguridad + Olvidar la billetera + Olvidar de todos modos + Esta billetera tiene una copia de seguridad. Asegúrese de poder recuperarla antes de olvidarla. + Si olvida esta billetera sin una copia de seguridad, perderá permanentemente el acceso a sus fondos. + ¿Estás seguro de que desea olvidar esta billetera? + Entiendo que si no he hecho una copia de seguridad de mi billetera antes de eliminarla, perderé el acceso a ella. + Entiendo que quitar mi billetera no la borra, solo la elimina de mi dispositivo. + Actualizar + No se requiere frase semilla. Su tarjeta o anillo Tangem se convierte en su copia de seguridad. + Copia de seguridad con Tangem + No se puede actualizar. Ya existe una billetera en este dispositivo. + Elija otro dispositivo. Este no se puede usar para la actualización. + Se produjo un error durante la operación. + Sus fondos permanecen seguros y totalmente accesibles durante el proceso + Acceso a los fondos + Los datos de su billetera se borrarán de la aplicación y se almacenarán en su billetera de hardware. + Seguridad general + Las claves privadas se transferirán de la aplicación a su billetera de hardware Tangem + Migración de claves + Escanear dispositivo + Iniciar actualización + Estás a punto de actualizarte a nuestra billetera de hardware. Mantendrá sus activos seguros en almacenamiento en frío. + Tangem Wallet + Actualice a nuestra billetera de hardware + Mantenga sus criptomonedas seguras con la billetera de hardware de primer nivel de Tangem. + Actualice su billetera a la seguridad del hardware Esta información fue generada con IA.\nPulse aquí si encuentra algún error. Para cambiar el código de acceso coloque la tarjeta o el anillo como se muestra arriba y no lo retire hasta el fin de la operación Toque para cambiar la contraseña @@ -1299,7 +1374,7 @@ Error al emitir la tarjeta Ha ocurrido un error técnico, por favor inténtalo de nuevo haciendo clic en el botón de abajo Ha ocurrido un error técnico, por favor contacta con el soporte - Usa tus criptomonedas para compras en el mundo real. \nEs una tarjeta de pago única en su tipo. + Obtén tu tarjeta virtual Tangem Visa gratuita Obtener Tangem Pay Ir a Soporte Suele tardar hasta 15 minutos @@ -1312,8 +1387,8 @@ KYC en curso Ver estado KYC en progreso para Tangem Pay - Usa tus criptomonedas para compras en el mundo real. \nEs una tarjeta de pago única en su tipo. - Tangem Visa Card + Obtén tu tarjeta virtual Tangem Visa gratuita + Usa USDC para pagos cotidianos Obtener tarjeta Con tarjeta digital que funciona con Apple Pay y Google Pay Gasta tus activos en cualquier lugar @@ -1323,15 +1398,15 @@ Privacidad inigualable Obtén tu tarjeta Tangem Pay gratuita en minutos Cuenta de pago - Sincronización de cuenta de pago necesaria + La cuenta de pago no está sincronizada Estamos solucionando un problema técnico. Por favor, inténtelo de nuevo más tarde. Servicio temporalmente no disponible No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando. Sincronización necesaria - Tangem Visa Card + Usa USDC para pagos cotidianos Tangem Pay temporalmente no disponible Tangem Pay - Usa tu tarjeta o anillo para restaurar el acceso a tu cuenta de pago + Haga clic en el botón de abajo para restaurar el acceso Tu código PIN Esta es mi billetera Saldos ocultos @@ -1412,6 +1487,7 @@ Active las notificaciones push y le avisaremos al instante cuando le lleguen fondos. No se pierda ninguna transacción Agregar una nueva billetera + Si olvida esta billetera sin una copia de seguridad, perderá permanentemente el acceso a sus fondos. ¿Estás seguro de que deseas olvidar esta billetera? Ha ocurrido un error, por favor escanee su tarjeta o anillo para iniciar sesión Esta billetera ya se ha guardado, puede agregar otro diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index dea8c07645..24e86ad9cd 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -72,7 +72,19 @@ Ajouter un wallet Sélectionnez un wallet pour vous connecter Heureux de te revoir! + Vous avez sauvegardé votre portefeuille avec succès. Ces mots ne peuvent pas être récupérés en cas de perte. Assurez-vous de les conserver en lieu sûr. + Sauvegarde terminée + Votre seed phrase est un ensemble fixe de %s mots aléatoires permettant d\'accéder à votre portefeuille et de le récupérer. + Ces mots ne peuvent être récupérés s\'ils sont perdus. Conservez-les précieusement. + Gardez-les en sécurité + Conservez ces %s mots dans un endroit sûr et ne les communiquez à personne. + Aucune récupération possible + Seed phrase + Ne communiquez jamais ces mots à qui que ce soit. Tangem ne vous les demandera jamais. Les %s mots ci-dessous constituent la seed phrase de votre portefeuille. Utilisez-les pour restaurer votre portefeuille si vous perdez votre appareil. + Notez ces %s mots dans l\'ordre numérique et conservez-les en lieu sûr et confidentiel. + Vous êtes entièrement responsable de la sécurité de votre portefeuille et de la sauvegarde de votre seed phrase. + Seed phrase Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres Ne plus afficher Compris @@ -488,6 +500,13 @@ Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s Donner l\'autorisation Illimité + Votre sauvegarde est créée à l\'aide de 2 ou 3 cartes Tangem. Conservez-les dans des endroits sûrs distincts afin de les protéger contre toute perte ou tout dommage. Aucune seed phrase n\'est nécessaire. + Sauvegarde avec plusieurs cartes + Ajouter le wallet Tangem + Votre clé privée est générée directement dans la carte Tangem et ne la quitte jamais. + Génération de clés + Toutes les opérations cryptographiques s\'effectuent à l\'intérieur de la puce sécurisée, certifiée contre le clonage et la falsification physique. + Sécurité au niveau matériel Ajouter un Portefeuille existant Créer un nouveau Portefeuille Commandez @@ -496,6 +515,73 @@ à %s Via %s Êtes-vous sûr de vouloir annuler la configuration du code d\'accès ? + Sauvegarder maintenant + Pour terminer la configuration, sauvegardez votre portefeuille et sécurisez l\'application à l\'aide d\'un code d\'accès. + Finaliser maintenant + Finaliser la configuration du portefeuille + Terminez la configuration en sécurisant l\'application à l\'aide d\'un code d\'accès. + Si vous quittez, vous devrez recommencer depuis le début. + Êtes-vous sûr de vouloir quitter le processus d\'installation ? + Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire. + Si vous le faites, vous devrez recommencer depuis le début. + Récupérer un portefeuille existant via la sauvegarde Google Drive + Sauvegarde Google Drive + Créez un portefeuille sécurisé et transférez vos fonds pour bénéficier d\'une protection supplémentaire. + Créer un nouveau portefeuille + Renforcez votre sécurité grâce au Hardware wallet haut de gamme Tangem. + Hardware wallet + Transférez votre portefeuille actuel vers Tangem. + Améliorer le wallet actuel + Aller à la sauvegarde + Veuillez sauvegarder votre portefeuille avant de créer un code d\'accès. + Terminez d\'abord la sauvegarde. + Finalisez d\'abord la sauvegarde. + Incomplet + Autres méthodes + Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds. + Seed phrase + Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde. + Pour passer à un portefeuille matériel, terminez le processus de sauvegarde. + Vos clés privées sont cryptées et stockées en toute sécurité sur votre téléphone. + Les clés privées restent sur votre appareil + Créez ou restaurez votre portefeuille à l\'aide d\'une seed phrase. + Sauvegarde de la seed phrase + Créer un wallet mobile + Importer un portefeuille existant + Cette seed phrase a déjà été importée. + Wallet mobile + Oubliez votre portefeuille + Ce portefeuille sera définitivement supprimé de votre appareil. + Êtes-vous sûr de vouloir faire cela ? + Oubliez votre portefeuille + Aller à la sauvegarde + Afficher la sauvegarde + Oubliez votre portefeuille + Oubliez quand même + Ce portefeuille dispose d\'une sauvegarde. Assurez-vous de pouvoir la récupérer avant d\'oublier le portefeuille. + Si vous oubliez ce portefeuille sans sauvegarde, vous perdrez définitivement l\'accès à vos fonds. + Êtes-vous sûr de vouloir oublier ce portefeuille ? + Je comprends que si je n\'ai pas sauvegardé mon portefeuille avant de le supprimer, je perdrai l\'accès à celui-ci. + Je comprends que le fait de supprimer mon portefeuille ne l\'efface pas, mais le supprime uniquement de mon appareil. + Améliorer + Aucune seed phrase n\'est requise. Votre carte ou bague Tangem devient votre sauvegarde sécurisée. + Sauvegarde avec Tangem + Impossible de mettre à niveau. Un portefeuille existe déjà sur cet appareil. + Choisissez un autre appareil. Celui-ci ne peut pas être utilisé pour la mise à niveau. + Une erreur s\'est produite pendant l\'opération. + Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus. + Accès aux fonds + Les données de votre wallet seront effacées de l\'application et stockées sur votre hardware wallet. + Sécurité générale + Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem. + Migration des clés + Scannez l\'appareil + Lancer la mise à niveau + Vous êtes sur le point de passer à notre hardware wallet. Il assurera la sécurité de vos actifs grâce au stockage hors ligne. + Tangem Wallet + Passez à notre Hardware Wallet + Protégez vos cryptomonnaies grâce au hardware wallet haut de gamme de Tangem. + Améliorez la sécurité de votre wallet grâce à un dispositif matériel Ces informations ont été générées avec l\'IA.\nAppuyez ici si vous trouvez des erreurs. Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe @@ -1280,7 +1366,7 @@ Échec de l\'émission de la carte Une erreur technique s\'est produite, veuillez réessayer en cliquant sur le bouton ci-dessous Une erreur technique s\'est produite, veuillez contacter le support - Utilisez vos cryptomonnaies pour vos dépenses quotidiennes. \nC\'est une carte de paiement unique en son genre. + Obtenez votre carte virtuelle Tangem Visa gratuite Obtenir Tangem Pay Contacter le support Cela prend généralement jusqu\'à 15 minutes @@ -1293,8 +1379,8 @@ KYC en cours Voir le statut KYC en cours pour Tangem Pay - Utilisez vos cryptomonnaies pour vos dépenses du quotidien. \nC\'est une carte de paiement unique en son genre. - Tangem Visa Card + Obtenez votre carte virtuelle Tangem Visa gratuite + Utilisez USDC pour les paiements quotidiens Obtenir la carte Avec carte numérique compatible Apple Pay et Google Pay Dépensez vos actifs partout @@ -1304,15 +1390,15 @@ Confidentialité inégalée Obtenez votre carte Tangem Pay gratuite en quelques minutes Compte de paiement - Synchronisation du compte de paiement nécessaire + Le compte de paiement n\'est pas synchronisé Nous réparons un problème technique. Veuillez réessayer plus tard. Service temporairement indisponible Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours. Synchronisation requise - Tangem Visa Card + Utilisez USDC pour les paiements quotidiens Tangem Pay est temporairement indisponible Tangem Pay - Utilisez votre carte ou votre bague pour restaurer l\'accès à votre compte de paiement + Cliquez sur le bouton ci-dessous pour restaurer l\'accès Votre code PIN C\'est mon portefeuille Soldes masqués @@ -1392,6 +1478,7 @@ Activez les notifications pour recevoir des alertes lorsque des fonds arrivent dans votre portefeuille. Ne manquez aucune transaction Ajouter un nouveau portefeuille + Si vous supprimez ce portefeuille sans sauvegarde, vous perdrez définitivement l\'accès à vos fonds. Êtes-vous sûr de vouloir supprimer ce portefeuille ? Une erreur s\'est produite, veuillez scanner votre carte ou bague pour vous connecter Ce portefeuille a déjà été enregistré, vous pouvez en ajouter un autre diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index b7536b13ca..2e418c1d4c 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -139,7 +139,7 @@ Impossibile emettere la carta Si è verificato un errore tecnico, riprova cliccando il pulsante qui sotto Si è verificato un errore tecnico, contatta il supporto - Utilizza le tue attività per fare acquisti nel mondo reale. \nÈ una carta di pagamento diversa da qualsiasi altra. + Ottieni la tua carta virtuale Tangem Visa gratuita Ottieni Tangem Pay Vai al supporto Di solito richiede fino a 15 minuti @@ -152,8 +152,8 @@ KYC in corso Visualizza stato KYC in corso per Tangem Pay - Utilizza le tue attività per fare acquisti nel mondo reale. \nÈ una carta di pagamento diversa da qualsiasi altra. - Tangem Visa Card + Ottieni la tua carta virtuale Tangem Visa gratuita + Usa USDC per i pagamenti quotidiani Ottieni carta Con carta digitale che funziona con Apple Pay e Google Pay Spendi i tuoi asset ovunque @@ -163,15 +163,15 @@ Privacy senza rivali Ottieni la tua carta Tangem Pay gratuita in pochi minuti Conto di pagamento - Sincronizzazione del conto di pagamento necessaria + Il conto di pagamento non è sincronizzato Stiamo risolvendo un problema tecnico. Riprova più tardi. Servizio temporaneamente non disponibile Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare. Sincronizzazione necessaria - Tangem Visa Card + Usa USDC per i pagamenti quotidiani Tangem Pay è temporaneamente non disponibile Tangem Pay - Usa la tua carta o il tuo anello per ripristinare l\'accesso al tuo conto di pagamento + Fare clic sul pulsante in basso per ripristinare l\'accesso Il tuo codice PIN Tangem Twin Imposta un codice di 4 cifre.\nVerrà utilizzato per i pagamenti. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 5ba1fbe88a..f884797c15 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -23,7 +23,7 @@ 回復する 「 %1$s 」を回復しようとしています。 アカウントを回復する - すでにアクティブアカウントの上限(20件)に達しています。復元するには、1つをアーカイブしてください。 + 有効なアカウントは最大20件までです。1つのアカウントをアーカイブすると、このアカウントを復元できます。 アカウントを復元できません アーカイブ済み アカウントをアーカイブできませんでした。しばらくしてからもう一度お試しください。 @@ -43,12 +43,13 @@ アカウントを追加 保存 アカウント名 - アカウント名はすでに存在しています + この名前のアカウントはすでに存在します。別の名前を選択してください。 + このアカウント名はすでに使用されています アカウント 新しいアカウント アカウントを追加 アカウントを編集 - 少し時間をおいて再度お試しください。問題が続く場合はサポートへご連絡ください。こちらで解決をお手伝いします。 + しばらく時間をおいてから、もう一度お試しください。問題が解決しない場合は、サポートにお問い合わせください。こちらで解決をお手伝いします。 %1$s ( %2$s内) メインアカウント すでにアクティブアカウントの上限%1$s件を超えています。復元するには、1つをアーカイブしてください。 @@ -592,11 +593,13 @@ アクセスコードを作成する前にウォレットをバックアップしてください。 先にバックアップを完了してください まずバックアップを完了する + 未完了 その他の方法 資金を保護するため、リカバリーフレーズは安全な場所に保管し、他人に知られないようにしてください。 リカバリーフレーズ アクセスコードでウォレットを保護するには、バックアップの手続きを完了してください。 ハードウェアウォレットにアップグレードするには、バックアップの手続きを完了してください。 + 秘密鍵は安全に暗号化され、スマートフォン上に保存されています 秘密鍵はデバイス上に保持されます リカバリーフレーズを使ってウォレットを作成または復元してください。 シードフレーズのバックアップ @@ -617,6 +620,7 @@ このウォレットを削除してもよろしいですか? ウォレットを削除する前にバックアップを行っていない場合、ウォレットへのアクセスを失うことを理解しています。 ウォレットを削除しても中身自体が消えるわけではなく、このデバイスから表示が消えるだけであることを理解しています。 + アップグレード シードフレーズは不要です。Tangemカードまたはリングが安全なバックアップとなります。 Tangemでバックアップ アップグレードできません。このデバイスにはすでにウォレットが存在します。 @@ -789,12 +793,16 @@ モバイルウォレットを作成するには、%1$sにアップデートする必要があります モバイルウォレットを使用するには、%1$s以降が必要です すべてのニュース + いいね %d時間前 %d分前 + クイックまとめ + 関連ニュース + 情報源 最新情報を入手 お使いのデバイスではNFCが使用できません NFTについて @@ -941,7 +949,7 @@ 通知 バックアップデバイスが1つ追加されました カードまたはリングを用意してください - バックアップデバイス2つが追加されました + バックアップデバイスが2つ追加されました 始めるには、ウォレットに任意の金額を入金するだけです 始めるには、ウォレットに%1$s %2$s以上入金するだけです 暗号資産を購入する @@ -1468,7 +1476,7 @@ カードの発行に失敗しました 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 技術的なエラーが発生しました。サポートへお問い合わせください。 - 暗号資産を日常の支払いに使おう。\n\nこれまでにないタイプの決済カード。 + 無料のTangem Visaバーチャルカードを入手 Tangem Payを入手 サポートへ移動 通常は最大で15分ほどかかります @@ -1476,14 +1484,16 @@ カードを発行しています カードを準備しています。少し時間がかかる場合があります。 Tangem Pay + 中止を確定する + KYC手続きを中止しますか?いつでも再開できます。 プロフィールを確認できませんでした。ご不明な点があればサポートまでお問い合わせください。 申し訳ございませんが、本人確認を行うことができませんでした KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 以下のボタンから、現在のKYCステータスを確認するか、KYCをキャンセルできます。 - 暗号資産を、リアルな支払いに。\n他とはまったく違う、新しいタイプの決済カード。 - Tangem Visaカード + 無料のTangem Visaバーチャルカードを入手 + 日常の支払いにUSDCを利用 カードをGET Apple PayとGoogle Payに対応したデジタルカード付き どこでも暗号資産を使える @@ -1493,15 +1503,15 @@ 他に類を見ないプライバシー 無料のTangem Payカードを数分でゲットしましょう 支払いアカウント - 支払いアカウントの同期が必要です + 支払アカウントが同期されていません 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 同期が必要です - Tangem Visaカード + 日常の支払いにUSDCを利用 Tangem Payは現在一時的に利用できません。 Tangem Pay - カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 + 下のボタンをクリックしてアクセスを復元してください PINコード これは私のウォレットです 残高非表示 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 537e6d8e15..6be32ab70f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -54,7 +54,7 @@ Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов. Продолжить Отменить - Вы уверены, что хотите создание нового аккаунта? + Вы уверены, что хотите отменить создание нового аккаунта? Вы уверены, что хотите отменить изменения? Несохраненные изменения Некоторые пользовательские токены были перемещены из «%1$s» в «%2$s», поскольку их путь деривации относится к этой учётной записи. @@ -1457,6 +1457,7 @@ Не удалось разморозить карту, попробуйте еще раз Карта разморожена Вывести + Запрещено использовать на root-устройствах Пополнить Способы пополнения Номер @@ -1484,6 +1485,7 @@ Всё готово! Можно пользоваться картой Добавьте карту в Google Pay Добавить карту в Apple Pay + ПИН-код Скопируйте свой адрес или покажите QR Техническая ошибка. Попробуйте позже или обратитесь в поддержку. Пополнение недоступно @@ -1492,6 +1494,7 @@ Пополните карту любым активом через обмен Реквизиты Разморозить карту + Ваш ПИН Вывести Вывод сейчас недоступен Вы не можете начать обмен или новый вывод, пока не завершится текущий. @@ -1502,7 +1505,7 @@ Не удалось выпустить карту Техническая ошибка, попробуйте ещё раз, нажав кнопку ниже Техническая ошибка, свяжитесь с поддержкой - Используйте криптовалюту в реальной жизни. \nКарта, не похожая ни на одну другую. + Откройте бесплатную виртуальную карту Tangem Visa Получить Tangem Pay Написать в поддержку Обычно это занимает до 15 минут @@ -1515,8 +1518,9 @@ KYC в процессе Посмотреть статус KYC в процессе для Tangem Pay - Используйте криптовалюту в реальной жизни. \nКарта, не похожая ни на одну другую. - Tangem Visa Card + Вы можете посмотреть текущий статус KYC или отменить его + Откройте бесплатную виртуальную карту Tangem Visa + Оплачивайте ежедневные покупки в USDC Открыть карту Виртуальную карту можно добавить в Apple Pay и Google Pay Покупайте где угодно @@ -1526,15 +1530,15 @@ Абсолютная приватность Откройте виртуальную \nTangem Pay Card Платежный аккаунт - Требуется синхронизация платежного аккаунта + Платежный аккаунт не синхронизирован Мы устраняем техническую проблему. Пожалуйста, попробуйте позже. Сервис временно недоступен Не можем показать данные карты, но оплаты продолжают работать. - Требуется синхронизация - Tangem Visa Card + Не синхронизирован + Оплачивайте ежедневные покупки в USDC Tangem Pay временно недоступен Tangem Pay - Используйте вашу карту или кольцо для восстановления доступа к платежному аккаунту + Нажмите на кнопку ниже, чтобы восстановить доступ Ваш PIN-код Это мой кошелек Балансы скрыты diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 92eff0a832..968fdbb71e 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -49,6 +49,19 @@ Системна Тема Налаштування застосунку + Ви успішно створили резервну копію свого гаманця. + Ці слова неможливо відновити у разі втрати. Зберігайте їх надійно. + Бекап завершено + Ваша фраза відновлення — це набір випадкових слів %s для доступу та відновлення гаманця. + Ці слова неможливо відновити, якщо їх загубити. Зберігайте їх у безпеці. + Зберігайте в безпеці + Збережіть ці %s слова в безпечному місці та нікому не розповідайте про них. + Відновлення неможливе + Фраза відновлення + Наведені нижче слова %s - це фраза для відновлення вашого гаманця. Ніколи і нікому не повідомляйте ці слова. Tangem ніколи не запитає їх у вас. Використовуйте їх, щоб відновити свій гаманець, якщо ви втратите пристрій. + Запишіть ці %s слова в указаному порядку і зберігайте їх у безпеці та таємниці. + Ви несете повну відповідальність за безпеку свого гаманця і фрази відновлення. + Фраза відновлення Щоб приховати або показати свій баланс, просто переверніть екран пристрою вниз або вимкніть його в налаштуваннях Більше не показувати Зрозуміло @@ -413,11 +426,85 @@ Щоб продовжити, вам потрібно дозволити смарт-контракту %1s використовувати ваш %2s Надати дозвіл Необмежено + Резервна копія створюється на 2–3 картках Tangem. Зберігайте їх окремо для безпеки — seed-фраза не потрібна. + Бекап на декілька карток + Додати гаманець Tangem + Ваш приватний ключ генерується на картці Tangem і ніколи не покидає її. + Генерація ключа + Операції з криптографією відбуваються всередині захищеного чіпу, стійкого до клонування та фізичному взлому. + Безпека на апаратному рівні Створити новий гаманець Купити Сканувати в %s В мережі %s + Ви впевнені, що хочете скасувати процес створення коду доступу? + Створити бекап + Щоб завершити налаштування, створіть резервну копію свого гаманця та захистіть додаток за допомогою коду доступу. + Завершити зараз + Завершити налаштування гаманця + Завершіть налаштування, захистивши додаток кодом доступу. + Якщо ви вийдете, вам доведеться починати спочатку. + Ви впевнені, що хочете вийти з процесу активації? + Зберігає ваші криптовалюти в безпеці та в режимі офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище. + Якщо ви це зробите, доведеться почати спочатку. + Відновлення існуючого гаманця за допомогою резервної копії Google Диску + Google Диск бекап + Створіть новий захищений гаманець і переведіть свої кошти для додаткового захисту. + Створити новий гаманець + Підвищіть рівень своєї безпеки за допомогою просунутого апаратного гаманця Tangem. + Апаратний гаманець + Перенесіть свій поточний гаманець у Tangem. + Оновіть поточний гаманець + Перейти до бекапу + Будь ласка, створіть резервну копію свого гаманця, перш ніж створювати код доступу. + Спочатку завершіть резервне копіювання + Спочатку завершіть резервне копіювання + Не завершено + Інші методи + Збережіть фразу відновлення у безпечному місці і тримайте її у таємниці. + Фраза відновлення + Щоб захистити свій гаманець за допомогою коду доступу, завершіть процес резервного копіювання. + Щоб покращити гаманець до апаратного, спочатку створіть резервну копію. + Ваші приватні ключі надійно зашифровані та зберігаються на вашому телефоні + Ключі зберігаються у застосунку + Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення. + Резервна копія + Створити мобільний гаманець + Імпортувати існуючий гаманець + Ця фраза відновлення вже була імпортована + Мобільний гаманець + Забути гаманець + Цей гаманець буде назавжди видалено з вашого пристрою + Ви впевнені, що хочете виконати цю операцію? + Забути гаманець + Перейти до резервної копії + Переглянути резервне копіювання + Забути гаманець + Все одно забути + Резервна копія цього гаманця існує. Перевірте її перед видаленням, щоб переконатися, що зможете відновити гаманець пізніше. + Якщо ви видалите цей гаманець без резервної копії, ви назавжди втратите доступ до своїх коштів. + Забути цей гаманець? + Я розумію, що якщо я не створив резервну копію гаманця перед його видаленням, я можу втратити досту до нього. + Я розумію, що видалення мого гаманця не видаляє його повністю, а просто видаляє його з мого пристрою. + Фраза відновлення більше не потрібна — ваша картка або кільце Tangem стає вашою безпечною резервною копією. + Резервне копіювання з Tangem + Цей пристрій не може бути використаний для оновлення, він вже містить інший гаманець. + Виберіть інший пристрій. Цей не можна використовувати для оновлення. + Під час операції виникла помилка. + Ваші кошти залишаються в безпеці та повністю доступними протягом усього процесу + Доступ до коштів + Данні вашого гаманця будуть видалені із застосунку і збережені на вашому пристрої Tangem. + Загальна безпека + Приватні ключі будуть переміщені з додатку у вашу Tangem картрку або кільце + Міграція ключів + Сканувати пристрій + Розпочати оновлення + Ви збираєтеся перейти на пристрій Tangem, де ваші активи будуть у безпеці в холодному сховищі. + Tangem Wallet + Оновіть до апаратного гаманця + Зберігайте свою криптовалюту в безпеці за допомогою першокласного апаратного гаманця Tangem. + Оновіть свій гаманець до апаратної версії. Ця інформація була створена за допомогою ШІ.\nНатисніть тут, якщо знайшли помилку. Щоб змінити код доступу, прикладіть картку або кільце, як показано вище, і не прибирайте її до закінчення операції Щоб змінити пароль, прикладіть картку, як показано вище, і не прибирайте її до закінчення операції @@ -1141,9 +1228,14 @@ Показати деталі Обміняйте будь-який актив у вашому портфелі на картку Розморозити картку - Tangem Visa Card + Отримайте безкоштовну віртуальну картку Tangem Visa + Отримайте безкоштовну віртуальну картку Tangem Visa + Використовуйте USDC для щоденних платежів + Платіжний рахунок не синхронізовано Ми усуваємо технічну проблему. Будь ласка, спробуйте пізніше. Сервіс тимчасово недоступний + Використовуйте USDC для щоденних платежів + Натисніть кнопку нижче, щоб відновити доступ Це мій гаманець Баланси приховано Баланси показано @@ -1220,6 +1312,7 @@ Нові функції та важливі новини Бажаєте використовувати Push-повідомлення? Додати новий гаманець + Якщо ви видалите цей гаманець без резервної копії, ви назавжди втратите доступ до своїх коштів. Ви впевнені, що хочете видалити цей гаманець? Сталася помилка, будь ласка, відскануйте свою картку або кільце, для входу Цей гаманець вже збережено, ви можете додати інший diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 6fa2c8b69c..a79d66c1d7 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -382,7 +382,7 @@ 无法发行卡片 出现技术错误,请点击下方按钮重试 出现技术错误,请联系客服 - 使用您的加密货币进行真实世界消费。\n这是一张与众不同的支付卡。 + 獲取您的免費 Tangem Visa 虛擬卡 获取Tangem Pay 前往客服中心 通常需要最多15分钟 @@ -395,8 +395,8 @@ KYC进行中 查看状态 Tangem Pay 的 KYC 正在進行中 - 使用您的加密货币进行真实世界消费。这是一张与众不同的支付卡。 - Tangem Visa Card + 獲取您的免費 Tangem Visa 虛擬卡 + 使用 USDC 進行日常支付 获取卡片 使用支援 Apple Pay 和 Google Pay 的數位卡 在任何地方花费您的资产 @@ -406,15 +406,15 @@ 無與倫比的隱私 在幾分鐘內獲得免費的 Tangem Pay 卡 付款帳戶 - 需要同步支付账户 + 付款帳戶未同步 我们正在修复技术问题。请稍后再试。 服務暫時無法使用 目前無法顯示資料,但卡片支付仍可正常使用。 需要同步 - Tangem Visa Card + 使用 USDC 進行日常支付 Tangem Pay暂时不可用 Tangem Pay - 使用您的卡片或戒指恢复对支付账户的访问 + 點擊下方按鈕以恢復存取權限 您的PIN码 隱藏 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 76cee235d8..1bce65560b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -23,12 +23,12 @@ Recover You’re about to recover “%1$s”. Recover account - You have already exceeded the limit of 20 active accounts. Archive one to recover. + You’ve reached the limit of 20 active accounts. Archive one account to recover this one. Can\'t recover account Archived We couldn’t archive account. Please try again later. - This account participates in the referral program. - This account cannot be archived. + This account is participating in the referral program. + This account can’t be archived We couldn’t create account. Please try again later. Account created Archive account @@ -43,12 +43,13 @@ Add account Save Account name - Account name already exists + An account with this name already exists. Please choose a different name. + Account name already in use Account 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. + Please try again later. If the problem persists, contact 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 @@ -127,15 +128,15 @@ Mobile Wallet You successfully backed up your wallet. - These words are unrecoverable if lost. Keep them somewhere safe. + These words cannot be recovered if lost. Store them securely. Backup completed - Your secret recovery phrase is a fixed set of %s random words for accessing and recovering your wallet. + Your secret recovery phrase is a set of %s random words for accessing and recovering your wallet. These words cannot be recovered if lost. Keep them safe. Keep it safe Save these %s words in a secure location and never share them with anyone. No recovery possible Recovery phrase - Never share these words with anyone. Tangem will never ask you for them. The %s words below are your wallet\'s recovery phrase. Use them to restore your wallet if you lose your device. + The %s words below are your wallet\'s recovery phrase. Never share these words with anyone. Tangem will never ask you for them. Use them to restore your wallet if you lose your device. Write down these %s words in numerical order and keep them safe and private You are fully responsible for securing your wallet and safely backing up your recovery phrase. Recovery phrase @@ -546,6 +547,7 @@ Please tell us what card or ring do you have Hi support team, Please tell us more about your issue. Every small detail can help. + Backup issue Previously activated wallet My suggestions Can\'t scan a card/ring @@ -564,8 +566,8 @@ To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited - Your backup is created using 2 or 3 Tangem cards. Keep them in separate safe places to protect against loss or damage — no seed phrase needed. - Backup with Multiple Cards + A backup is created using 2–3 Tangem cards. Store them separately in secure locations to protect against loss or damage. No seed phrase needed. + Backup With Multiple Cards Add Tangem Wallet Your private key is generated directly inside the Tangem card and never leaves it. Key Generation @@ -585,7 +587,7 @@ Finalize wallet setup Complete setup by securing the app with an access code. If you exit, you\'ll need to start over. - Are you sure you want to quit the setup process? + Are you sure you want to quit activation? Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup @@ -598,7 +600,7 @@ Upgrade current wallet Go to backup Please back up your wallet before creating an access code. - Finish backup first + Complete the backup first Finalize backup first Incomplete Other methods @@ -627,14 +629,15 @@ Are you sure you want to forget this wallet? I understand that if I haven\'t backed up my wallet before removing it, I will lose access to it. I understand that removing my wallet does not delete it, only removes it from my device. + Upgrade Seed phrase not required. Your Tangem card or ring becomes your secure backup. - Backup with Tangem + Backup With Tangem Can\'t upgrade. A wallet already exists on this device. - Pick another device. This one can’t be used for the upgrade. + Pick another device. This one can\'t be used for the upgrade. An error occurred during the operation. Your funds remain safe and fully accessible during the process Access to funds - Your wallet data will be erased from the app and stored on your hardware wallet + Your wallet information will be erased from the app and stored on your hardware wallet General security Private keys will be moved from the app to your Tangem hardware wallet Key migration @@ -802,6 +805,7 @@ You must update to %1$s before creating a mobile wallet Mobile Wallet requires %1$s or later All news + Like %dh ago %dh ago @@ -810,6 +814,9 @@ %d minute ago %d minutes ago + Quick recap + Related News + Sources Stay in the loop NFC is not available on your device About NFT @@ -893,8 +900,8 @@ Please repeat the operation. The card will be reset to factory settings. Activation error Add tokens - You\'ve added one backup card or ring. When backup process is finished you can\'t add more backup devices. If you have one more card or ring, add it to the backup. Would you like to continue the backup process? - The backup process is partly complete. You can\'t exit it now. + You\'ve added one backup card or ring. Once backup is finalized, you can\'t add more devices. If you have one more card or ring, add it now. Do you want to continue? + The backup is partially complete and can\'t be quit now. A passphrase is an optional security feature that adds a word or phrase to your recovery phrase, creating a new set of wallet addresses for extra protection. Add a card or ring Scan card @@ -944,7 +951,7 @@ Legacy To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words So, let’s check - To start the backup process add up to two backup cards or rings. + To start the backup process, add up to two backup cards or rings. You can add one more card or ring or finalize the backup process Prepare the backup card with number %s Scan the primary card or ring to start the backup process. @@ -1083,8 +1090,8 @@ I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards of the current wallet I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery - Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code. - Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet. + A factory reset completely erases the wallet from the selected card or ring. You will not be able to restore the current wallet or use this card or ring to recover the access code. + A factory reset completely erases the wallet from the selected card or ring and removes it from the app. You will not be able to restore the current wallet. All Tangem devices have been reset. Something went wrong with the activation process. Please reset the cards one by one. Card verification failed @@ -1369,7 +1376,7 @@ Your stakes Store your crypto assets secure while keeping private keys contained in your card or ring Revolutionary Hardware Wallet - Up to 3 physical cards or rings to one wallet + Add up to 3 cards or rings to one wallet Ultra Secure Backup A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously — all in one card or ring Thousands of Currencies @@ -1492,7 +1499,7 @@ Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. - Use your crypto for real world spending. \nIt\'s a payment card unlike any other. + Get your free Tangem Visa virtual card Get Tangem Pay Go to Support It usually takes up to 15 minutes @@ -1508,8 +1515,8 @@ View Status KYC in progress for Tangem Pay Use the buttons below to view your current KYC status or cancel it. - Use your crypto for real world spending. \nIt’s a payment card unlike any other. - Tangem Visa Card + Get your free Tangem Visa virtual card + Use USDC for everyday payments Get card With digital card that works with Apple Pay and Google Pay Spend your assets anywhere @@ -1519,15 +1526,16 @@ Unrivaled privacy Get your free Tangem Pay Card in minutes Payment account - Payment account sync needed + Payment account is not synced We’re fixing a technical issue. Please try again later. Service temporarily unavailable Unable to display details. However, card payments are still working. - Sync needed - Tangem Visa Card + Not synced + Restore access + Use USDC for everyday payments Tangem Pay is temporarily unreachable Tangem Pay - Use your card or ring to restore access to your payment account + Click the button below to restore access Your PIN code This is my wallet Balances hidden @@ -1777,7 +1785,7 @@ Use %s or scan a card/ring to unlock access to your wallet The permission-granting process is currently underway and will be completed shortly Approval in Progress - It seems that the card or ring activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card or ring to your device. Please contact our Support team for assistance. + Activation was not completed successfully. This may be due to an NFC issue or incorrect tapping. Please contact our Support team for assistance. Activation error On December 3, 2024, the BEP-2 network was disabled by decision of the network developers and is no longer supported BNB Beacon Chain shut down @@ -1788,6 +1796,8 @@ Ok, Got it! Really cool! Refresh + According to Clore’s official documentation, all tokens received before December 21 will be migrated to Clore (ERC20); tokens received after will not. Transfer solution coming — stay tuned. + Clore Network Migration You are currently in the Demo mode Demo mode active The card you scanned is a developer card. Do not use it to create your wallet. @@ -1836,7 +1846,7 @@ The network is currently unreachable. Please try again later. Network is unreachable Top up your wallet - Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now. + Your wallet isn\'t backed up yet. Back it up now to protect your assets. Missing backup This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. Card has already signed transactions @@ -1976,10 +1986,10 @@ Use a Tangem hardware wallet Learn more & buy Discard - You have an interrupted backup. Do you want to resume? + Your backup was interrupted. Do you want to resume? Yes, resume Discard - If you discard the backup now, then you will have to reset the devices to factory settings to start over again + If you discard the backup now, you will have to reset the devices to factory settings to start over again Resume backup This is an irreversible action Log in with %s 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 73a2e96b23..10590d9f19 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 @@ -151,13 +151,14 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class TangemPayRefreshNeeded( - @DrawableRes val tangemIcon: Int?, - val onRefreshClick: () -> Unit, + @DrawableRes private val tangemIcon: Int?, + private val onRefreshClick: () -> Unit, + private val buttonText: TextReference, ) : Warning( title = resourceReference(id = R.string.tangempay_payment_account_sync_needed), subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), buttonsState = ButtonsState.PrimaryButtonConfig( - text = resourceReference(id = R.string.home_button_scan), + text = buttonText, iconResId = tangemIcon, onClick = onRefreshClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index b3f487ec56..1a2c03db4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -1,5 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState @@ -8,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState internal class TangemPayRefreshNeededStateTransformer( userWalletId: UserWalletId, + private val userWallet: UserWallet, private val onRefreshClick: () -> Unit, ) : WalletStateTransformer(userWalletId = userWalletId) { @@ -15,6 +18,10 @@ internal class TangemPayRefreshNeededStateTransformer( val tangemPayState = TangemPayState.RefreshNeeded( notification = TangemPayRefreshNeeded( tangemIcon = R.drawable.ic_tangem_24, + buttonText = when (userWallet) { + is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) + is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) + }, onRefreshClick = onRefreshClick, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 8b890929a2..6558e26c93 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -46,6 +46,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( stateController.update( transformer = TangemPayRefreshNeededStateTransformer( userWalletId = userWalletId, + userWallet = userWallet, onRefreshClick = { clickIntents.onRefreshPayToken(userWalletId) }, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt index fc3d9c852b..5b35cfb683 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt @@ -68,6 +68,7 @@ private fun TangemPayRefreshBlockPreview() { state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( tangemIcon = R.drawable.ic_tangem_24, + buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), onRefreshClick = {}, ), ), From 319f62f0082033b831ff7bce8869c09391361327 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Dec 2025 17:45:47 +0500 Subject: [PATCH 120/130] Updated on 2026-08-14 --- .../main/assets/configs/feature_toggles_config.json | 4 ++++ data/visa/build.gradle.kts | 3 +++ .../data/pay/DefaultTangemPayEligibilityManager.kt | 12 +++++------- .../com/tangem/data/pay/di/TangemPayDataModule.kt | 3 +++ domain/visa/build.gradle.kts | 3 +++ .../TangemPayMainScreenCustomerInfoUseCase.kt | 13 ++++++++++--- .../tangem/features/details/model/DetailsModel.kt | 2 +- .../tangem/features/details/utils/ItemsBuilder.kt | 11 +++++++---- .../features/tangempay/TangemPayFeatureToggles.kt | 1 + .../tangempay/DefaultTangemPayFeatureToggles.kt | 2 ++ .../wallet/model/intents/TangemPayClickIntents.kt | 11 ++++++++++- 11 files changed, 49 insertions(+), 16 deletions(-) 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 dce534df8d..1cb7bb7d24 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 @@ -39,6 +39,10 @@ "name": "TANGEM_PAY_ENABLED", "version": "5.31.0" }, + { + "name": "TANGEM_PAY_ENTRYPOINT_ENABLED", + "version": "undefined" + }, { "name": "NEW_TOKEN_RECEIVE_ENABLED", "version": "5.28.0" diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index f893a530d6..e1e1568878 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -39,6 +39,9 @@ dependencies { /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) + /** Feature API - remove after removing [TangemPayFeatureToggles] */ + implementation(projects.features.tangempay.details.api) + /** Project - Utils */ implementation(projects.core.utils) implementation(projects.domain.legacy) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 4ab4d074f2..7ccc87275c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -9,15 +9,10 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import javax.inject.Inject @@ -27,6 +22,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val onboardingRepository: OnboardingRepository, ) : TangemPayEligibilityManager { @@ -130,6 +126,8 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } private suspend fun checkTangemPayEligibility(): Boolean { + if (!tangemPayFeatureToggles.isEntryPointsEnabled) return true + return onboardingRepository.getCustomerEligibility() || onboardingRepository.checkCustomerEligibility() } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 719542728a..93f2455ffb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.security.DeviceSecurityInfoProvider import dagger.Binds import dagger.Module @@ -77,12 +78,14 @@ internal interface TangemPayDataModule { customerOrderRepository: CustomerOrderRepository, tangemPayOnboardingRepository: OnboardingRepository, eligibilityManager: TangemPayEligibilityManager, + tangemPayFeatureToggles: TangemPayFeatureToggles, deviceSecurity: DeviceSecurityInfoProvider, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( onboardingRepository = repository, customerOrderRepository = customerOrderRepository, eligibilityManager = eligibilityManager, + tangemPayFeatureToggles = tangemPayFeatureToggles, deviceSecurity = deviceSecurity, ) } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3c13ebe644..3dfad027a7 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -25,6 +25,9 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + /** Feature API - remove after removing [TangemPayFeatureToggles] */ + implementation(projects.features.tangempay.details.api) + /** Security */ implementation(deps.spongecastle.core) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 16ebd064e6..78d8486b32 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.pay.model.* import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import kotlinx.coroutines.flow.* @@ -24,6 +25,7 @@ class TangemPayMainScreenCustomerInfoUseCase( private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, private val eligibilityManager: TangemPayEligibilityManager, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val deviceSecurity: DeviceSecurityInfoProvider, ) { @@ -46,7 +48,7 @@ class TangemPayMainScreenCustomerInfoUseCase( .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") - if (error is VisaApiError.NotPaeraCustomer) { + if (error is VisaApiError.NotPaeraCustomer && tangemPayFeatureToggles.isEntryPointsEnabled) { showOnboardingBannerIfEligible(userWalletId) } else { updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) @@ -64,8 +66,13 @@ class TangemPayMainScreenCustomerInfoUseCase( .map(MainCustomerInfoContentState::Content) updateState(userWalletId, result) } else { - // if there's no tangem pay, check eligibility and show onboarding banner - showOnboardingBannerIfEligible(userWalletId) + if (tangemPayFeatureToggles.isEntryPointsEnabled) { + // if there's no tangem pay, check eligibility and show onboarding banner + showOnboardingBannerIfEligible(userWalletId) + } else { + // ignore if there's no TangemPay + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } } }, ) 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 992afae4ac..1c4c53f372 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 @@ -248,7 +248,7 @@ internal class DetailsModel @Inject constructor( } private fun addTangemPayItemIfEligible() { - if (!tangemPayFeatureToggles.isTangemPayEnabled) return + if (!tangemPayFeatureToggles.isEntryPointsEnabled) return modelScope.launch { val isEligible = tangemPayEligibilityManager .getEligibleWallets(shouldExcludePaeraCustomers = true) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index c8627bf1ea..7ee2b6eccd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -54,10 +54,13 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { } fun removeTangemPayItem(items: ImmutableList): ImmutableList { - val tangemPayItem = items.find { it.id == TANGEM_PAY_ITEM_ID } ?: return items - return items.toMutableList() - .also { list -> list.remove(tangemPayItem) } - .toImmutableList() + return items.map { block -> + if (block is DetailsItemUM.Basic && block.items.any { it.id == TANGEM_PAY_ITEM_ID }) { + block.copy(items = block.items.filter { it.id != TANGEM_PAY_ITEM_ID }.toImmutableList()) + } else { + block + } + }.toImmutableList() } private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt index 393e589bce..0345c3cb32 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.tangempay interface TangemPayFeatureToggles { val isTangemPayEnabled: Boolean + val isEntryPointsEnabled: Boolean } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt index a51c11a3bc..9b909186a0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -7,4 +7,6 @@ internal class DefaultTangemPayFeatureToggles( ) : TangemPayFeatureToggles { override val isTangemPayEnabled get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") + override val isEntryPointsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENTRYPOINT_ENABLED") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 7fc89a52ab..1a485dbf1d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -14,6 +14,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -55,6 +56,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, + private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), TangemPayIntents { @@ -176,7 +178,14 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( } override fun onOnboardingBannerClick(userWalletId: UserWalletId) { - router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) + modelScope.launch { + val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() + if (isEligible) { + router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) + } else { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + } + } } override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) { From 00032d7d11e0e092a63e3653951c9a7edd26452b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Dec 2025 18:22:53 +0500 Subject: [PATCH 121/130] Updated on 2026-08-14 --- .../components/notifications/Notification.kt | 2 ++ .../notifications/NotificationConfig.kt | 1 + .../model/intents/TangemPayClickIntents.kt | 22 +++++++++++++---- .../wallet/state/model/WalletNotification.kt | 2 ++ .../TangemPayRefreshNeededStateTransformer.kt | 1 + ...TangemPayRefreshShowProgressTransformer.kt | 24 +++++++++++++++++++ .../subscribers/TangemPayMainSubscriber.kt | 2 +- .../components/visa/TangemPayRefreshBlock.kt | 1 + 8 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 20bb9fce3b..d1b06a5538 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -296,6 +296,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, enabled = isEnabled, + showProgress = config.shouldShowProgress, ) } else { PrimaryButton( @@ -304,6 +305,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, enabled = isEnabled, + showProgress = config.shouldShowProgress, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 57338f98b4..9da575b6c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -38,6 +38,7 @@ data class NotificationConfig( val additionalText: TextReference? = null, @DrawableRes val iconResId: Int? = null, val onClick: () -> Unit, + val shouldShowProgress: Boolean = false, ) : ButtonsState() data class SecondaryButtonConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1a485dbf1d..c9c81a2859 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayEligibilityManager @@ -20,6 +21,8 @@ import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,7 +31,7 @@ internal interface TangemPayIntents { suspend fun onPullToRefresh() - fun onRefreshPayToken(userWalletId: UserWalletId) + fun onRefreshPayToken(userWallet: UserWallet) fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) @@ -70,10 +73,21 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } - override fun onRefreshPayToken(userWalletId: UserWalletId) { + override fun onRefreshPayToken(userWallet: UserWallet) { + stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId)) + modelScope.launch { - produceInitialDataTangemPay.invoke(userWalletId) - tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + produceInitialDataTangemPay.invoke(userWallet.walletId) + .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) } + .onLeft { + stateHolder.update( + transformer = TangemPayRefreshNeededStateTransformer( + userWallet = userWallet, + userWalletId = userWallet.walletId, + onRefreshClick = { onRefreshPayToken(userWallet) }, + ) + ) + } } } 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 10590d9f19..e2dc6648fc 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 @@ -154,6 +154,7 @@ sealed class WalletNotification(val config: NotificationConfig) { @DrawableRes private val tangemIcon: Int?, private val onRefreshClick: () -> Unit, private val buttonText: TextReference, + private val shouldShowProgress: Boolean, ) : Warning( title = resourceReference(id = R.string.tangempay_payment_account_sync_needed), subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), @@ -161,6 +162,7 @@ sealed class WalletNotification(val config: NotificationConfig) { text = buttonText, iconResId = tangemIcon, onClick = onRefreshClick, + shouldShowProgress = shouldShowProgress, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index 1a2c03db4d..98a3ee0caa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -23,6 +23,7 @@ internal class TangemPayRefreshNeededStateTransformer( is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) }, onRefreshClick = onRefreshClick, + shouldShowProgress = false, ), ) return if (prevState is WalletState.MultiCurrency.Content) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt new file mode 100644 index 0000000000..cd5885b406 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayRefreshShowProgressTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + val multiContentState = prevState as? WalletState.MultiCurrency.Content ?: return prevState + val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState + val refreshNotification = + refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState + + return multiContentState.copy( + tangemPayState = refreshNeededState.copy( + notification = refreshNotification.copy(shouldShowProgress = true), + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 6558e26c93..e6f5955bb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -47,7 +47,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( transformer = TangemPayRefreshNeededStateTransformer( userWalletId = userWalletId, userWallet = userWallet, - onRefreshClick = { clickIntents.onRefreshPayToken(userWalletId) }, + onRefreshClick = { clickIntents.onRefreshPayToken(userWallet) }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt index 5b35cfb683..b3c06f3a28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt @@ -70,6 +70,7 @@ private fun TangemPayRefreshBlockPreview() { tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), onRefreshClick = {}, + shouldShowProgress = true, ), ), modifier = Modifier, From 258e950178ef9d677e46b41c74b0009b5290ed65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Dec 2025 15:29:35 +0500 Subject: [PATCH 122/130] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 6 ++ .../api/pay/models/request/GetPinResponse.kt | 14 +++ .../models/response/CardDetailsResponse.kt | 1 - core/res/src/main/res/values-de/strings.xml | 6 +- core/res/src/main/res/values-es/strings.xml | 6 +- core/res/src/main/res/values-fr/strings.xml | 6 +- core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 6 +- .../src/main/res/values-uk-rUA/strings.xml | 5 +- core/res/src/main/res/values/strings.xml | 3 +- .../repository/DefaultOnboardingRepository.kt | 24 +++-- .../DefaultTangemPayCardDetailsRepository.kt | 87 ++++++++++--------- .../repository/TangemPayRequestPerformer.kt | 33 ------- .../TangemPayMainScreenCustomerInfoUseCase.kt | 4 - .../model/TangemPayChangePinModel.kt | 11 ++- .../TangemPayViewPinErrorStateTransformer.kt | 8 +- .../model/intents/TangemPayClickIntents.kt | 2 +- 18 files changed, 125 insertions(+), 99 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetPinResponse.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 1054ffef58..773dbb7b72 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -62,6 +62,12 @@ interface TangemPayApi { @Body body: CardDetailsRequest, ): ApiResponse + @GET("v1/customer/card/pin") + suspend fun getPin( + @Header("Authorization") authHeader: String, + @Header("X-Session-Id") sessionId: String, + ): ApiResponse + @PUT("v1/customer/card/pin") suspend fun setPin( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetPinResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetPinResponse.kt new file mode 100644 index 0000000000..ad9da0eda5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetPinResponse.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetPinResponse(@Json(name = "result") val result: Result?) { + + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "secret") val secret: String, + @Json(name = "iv") val iv: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt index 17b226bacc..599d94d6b1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt @@ -17,7 +17,6 @@ data class CardDetailsResponse( @Json(name = "card_number_end") val cardNumberEnd: String, @Json(name = "pan") val pan: Secret, @Json(name = "cvv") val cvv: Secret, - @Json(name = "pin") val pin: Secret?, ) @JsonClass(generateAdapter = true) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index cefd04ae41..dcdf6d4a87 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -449,7 +449,7 @@ Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen Standardadresse - Legacy adresse + Legacy %s adresse Empfangen von Vermögenswerten %s Adresse Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust. @@ -542,6 +542,7 @@ Bitte sag uns, welche Karte oder Ring du hast Hallo Support-Team, Bitte erzähle uns mehr über dein Problem. Jedes kleine Detail kann helfen. + Problem mit der Sicherung Zuvor aktivierte Wallet Meine Vorschläge Kann eine Karte oder Ring nicht scannen @@ -1502,6 +1503,7 @@ Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten Zahlungskonto Zahlungskonto ist nicht synchronisiert + Ungültige PIN: Sequenzen oder Wiederholungen vermeiden Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut. Service vorübergehend nicht verfügbar Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. @@ -1770,6 +1772,8 @@ OK, habe ich verstanden! Echt toll! Aktualisieren + Laut der offiziellen Dokumentation von Clore werden alle Münzen, die vor dem 21. Dezember erhalten wurden, in Clore (ERC-20 Token) migriert; Münzen, die nach diesem Datum erhalten wurden, nicht. Eine Lösung für den Transfer ist in Arbeit — bleibt dran. + Migration des Clore-Netzwerks Du befindest sich derzeit im Demo-Modus Demo-Modus aktiv Die Karte, die du gescannt hast, ist eine Entwicklerkarte. Verwenden diese nicht zur Erstellung Ihrer Wallet. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c1cf00a410..1c463c9d8e 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -401,7 +401,7 @@ Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Dirección por defecto - Dirección Legacy + Dirección %s Legacy Recibir activos %s dirección Enviar activos a otras redes resultará en una pérdida permanente. @@ -491,6 +491,7 @@ Por favor, díganos qué tarjeta o anillo tiene Hola equipo de soporte, Por favor, cuéntenos más sobre tu problema. Cada pequeño detalle puede ayudar. + Problema con la copia de seguridad Billetera previamente activada Mis recomendaciones No se puede escanear una tarjeta/anillo @@ -1399,6 +1400,7 @@ Obtén tu tarjeta Tangem Pay gratuita en minutos Cuenta de pago La cuenta de pago no está sincronizada + PIN no válido: evitar secuencias o repeticiones Estamos solucionando un problema técnico. Por favor, inténtelo de nuevo más tarde. Servicio temporalmente no disponible No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando. @@ -1582,6 +1584,8 @@ Entendido ¡Realmente genial! Actualizar + Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. + Migración de la red Clore Actualmente estás en el modo Demo Modo demo activo La tarjeta que ha escaneado es una tarjeta de desarrollador. No la use para crear su billetera. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 24e86ad9cd..16ebbde2fe 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -394,7 +394,7 @@ Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation Adresse par défaut - Legacy adresse + Legacy %s adresse Recevoir des actifs %s adresse L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive. @@ -482,6 +482,7 @@ Veuillez nous dire quelle carte vous avez Chère équipe de support, Veuillez nous en dire plus sur votre problème. Chaque petit détail peut nous aider. + Problème de sauvegarde Portefeuille précédemment activé Mes suggestions Impossible de scanner une carte @@ -1391,6 +1392,7 @@ Obtenez votre carte Tangem Pay gratuite en quelques minutes Compte de paiement Le compte de paiement n\'est pas synchronisé + Code PIN invalide : évitez les séquences ou les répétitions Nous réparons un problème technique. Veuillez réessayer plus tard. Service temporairement indisponible Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours. @@ -1592,6 +1594,8 @@ Ok, compris! Vraiment cool ! Rafraîchir + Selon la documentation officielle de Clore, toutes les pièces reçues avant le 21 décembre seront migrées vers Clore (token ERC-20) ; les pièces reçues après cette date ne le seront pas. Une solution de transfert arrive — restez à l\'écoute. + Migration du réseau Clore Vous êtes actuellement en mode démo Mode démo actif La carte que vous avez scannée est une carte de développeur. Ne l\'utilisez pas pour créer votre portefeuille. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 2e418c1d4c..9128efeb7f 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -164,6 +164,7 @@ Ottieni la tua carta Tangem Pay gratuita in pochi minuti Conto di pagamento Il conto di pagamento non è sincronizzato + PIN non valido: evitare sequenze o ripetizioni Stiamo risolvendo un problema tecnico. Riprova più tardi. Servizio temporaneamente non disponibile Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index f884797c15..666fe8e6bb 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1504,6 +1504,7 @@ 無料のTangem Payカードを数分でゲットしましょう 支払いアカウント 支払アカウントが同期されていません + 無効な暗証番号:連続や繰り返しを避けてください 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6be32ab70f..ee6d3b99d7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -460,7 +460,7 @@ Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования Основной адрес - Legacy адрес + Legacy %s адрес Получить активы %s адрес Отправка средств в другой сети может повлечь потерю средств. @@ -551,6 +551,7 @@ Скажите, пожалуйста, какая у вас карта или кольцо? Привет, команда поддержки, Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь. + Проблема резервного копирования Ранее активированный кошелек Мои предложения Не могу отсканировать карту/кольцо @@ -1531,6 +1532,7 @@ Откройте виртуальную \nTangem Pay Card Платежный аккаунт Платежный аккаунт не синхронизирован + Слабый ПИН: не используйте повторы или последовательности. Мы устраняем техническую проблему. Пожалуйста, попробуйте позже. Сервис временно недоступен Не можем показать данные карты, но оплаты продолжают работать. @@ -1739,6 +1741,8 @@ Понятно! Очень круто! Обновить + Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями. + Миграция сети Clore Вы находитесь в режиме демо Демо режим включен Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 968fdbb71e..6df04c2899 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -323,7 +323,7 @@ Перевірте підключення до інтернету або змініть мережу Умови використання Основна адреса - Legacy адреса + Legacy %s адреса Отримати активи %s адреса Надсилання активів в інші мережі призведе до безповоротної втрати. @@ -408,6 +408,7 @@ Розкажіть, будь ласка, яку картку або кільце ви маєте? Привіт, команда підтримки, Будь ласка, розкажіть нам більше про вашу проблему. Кожна дрібниця може допомогти. + Проблема з резервним копіюванням Раніше активований гаманець Мої пропозиції Не вдається відсканувати картку/кільце @@ -1383,6 +1384,8 @@ Зрозуміло! Дуже круто! Оновити + Згідно з офіційною документацією Clore, усі монети, отримані до 21 грудня, будуть мігровані в токен Clore (ERC-20); монети, отримані після цієї дати, — ні. Рішення для переказу перебуває в розробці — стежте за оновленнями. + Міграція мережі Clore Ви перебуваєте в демонстраційному режимі Демонстраційний режим активовано Відсканована вами картка є карткою розробника. Не використовуйте її для створення гаманця. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1bce65560b..1a5e95cd53 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1527,6 +1527,7 @@ Get your free Tangem Pay Card in minutes Payment account Payment account is not synced + Invalid PIN: avoid sequences or repeats We’re fixing a technical issue. Please try again later. Service temporarily unavailable Unable to display details. However, card payments are still working. @@ -1796,7 +1797,7 @@ Ok, Got it! Really cool! Refresh - According to Clore’s official documentation, all tokens received before December 21 will be migrated to Clore (ERC20); tokens received after will not. Transfer solution coming — stay tuned. + According to Clore’s official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned. Clore Network Migration You are currently in the Demo mode Demo mode active diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index fe06e7b5e3..9b12c88cb6 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.pay.repository import arrow.core.Either +import arrow.core.raise.catch import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest @@ -23,6 +24,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -110,15 +112,21 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun createOrder(userWalletId: UserWalletId) = withContext(dispatcherProvider.io) { launch { - requestHelper.runWithErrorLogs(TAG) { - val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId) - val result = requestHelper.request(userWalletId) { authHeader -> - tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) - }.result ?: error("Create order result is null") + catch( + block = { + val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId) + val response = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) + }.getOrNull() - val customerWalletAddress = requireNotNull(result.data.customerWalletAddress) - tangemPayStorage.storeOrderId(customerWalletAddress, result.id) - } + val result = requireNotNull(response?.result) + val customerWalletAddress = requireNotNull(result.data.customerWalletAddress) + tangemPayStorage.storeOrderId(customerWalletAddress, result.id) + }, + catch = { + Timber.tag(TAG).e("createOrder: $it") + }, + ) } } 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 a05f92ee7a..3252c49c4e 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 @@ -120,27 +120,18 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( block = { val publicKeyBase64 = getPublicKeyBase64() val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) - val result = requireNotNull( - requestHelper.performRequest(userWalletId = userWalletId) { authHeader -> - tangemPayApi.revealCardDetails( - authHeader = authHeader, - body = CardDetailsRequest(sessionId = sessionId), - ) - }.getOrNull()?.result, - ) + val response = requestHelper.performRequest(userWalletId = userWalletId) { authHeader -> + tangemPayApi.getPin(authHeader = authHeader, sessionId = sessionId) + }.getOrNull() + val result = requireNotNull(response?.result) + + val pin = rainCryptoUtil.decryptPin( + base64Secret = result.secret, + base64Iv = result.iv, + secretKeyBytes = secretKeyBytes, + ).takeIf { !it.isNullOrEmpty() } - val encryptedPin = result.pin - val pin = if (encryptedPin != null) { - rainCryptoUtil.decryptPin( - base64Secret = encryptedPin.secret, - base64Iv = encryptedPin.iv, - secretKeyBytes = secretKeyBytes, - ).takeIf { !it.isNullOrEmpty() } - } else { - null - } secretKeyBytes.fill(0) - pin.right() }, catch = ::catchException, @@ -148,14 +139,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either { - return requestHelper.runWithErrorLogs(TAG) { - val publicKeyBase64 = getPublicKeyBase64() - val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) - val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes) - secretKeyBytes.fill(0) + return catch( + block = { + val publicKeyBase64 = getPublicKeyBase64() + val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) + val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes) + secretKeyBytes.fill(0) - val status = requireNotNull( - requestHelper.request(userWalletId) { authHeader -> + val response = requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.setPin( authHeader = authHeader, body = SetPinRequest( @@ -164,27 +155,41 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( iv = encryptedData.ivBase64, ), ) - }.result?.result, - ) - when (status) { - SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS - SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK - SetPinResult.DECRYPTION_ERROR.name -> SetPinResult.DECRYPTION_ERROR - else -> SetPinResult.UNKNOWN_ERROR - } - } + }.getOrNull() + val status = requireNotNull(response?.result?.result) + val result = when (status) { + SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS + SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK + SetPinResult.DECRYPTION_ERROR.name -> SetPinResult.DECRYPTION_ERROR + else -> SetPinResult.UNKNOWN_ERROR + } + result.right() + }, + catch = ::catchException, + ) } override suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either { - return requestHelper.runWithErrorLogs(TAG) { - storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId)) - } + return catch( + block = { + storage.getAddToWalletDone( + customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId), + ).right() + }, + catch = ::catchException, + ) } override suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either { - return requestHelper.runWithErrorLogs(TAG) { - storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId), isDone = true) - } + return catch( + block = { + storage.storeAddToWalletDone( + customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId), + isDone = true, + ).right() + }, + catch = ::catchException, + ) } override suspend fun freezeCard( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 95b6decdf8..a788639680 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -18,7 +18,6 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -41,38 +40,6 @@ internal class TangemPayRequestPerformer @Inject constructor( private val customerWalletAddresses = ConcurrentHashMap() private val tokensMutex = Mutex() - @Deprecated("Do not use this method") - suspend fun runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either { - return try { - val result = requestBlock() - Either.Right(result) - } catch (exception: Exception) { - when (exception) { - is CancellationException -> { - throw exception - } - else -> { - Timber.tag(tag).e(exception) - Either.Left(errorConverter.convert(exception)) - } - } - } - } - - @Deprecated("Use perform request instead", replaceWith = ReplaceWith("performRequest")) - suspend fun request( - userWalletId: UserWalletId, - requestBlock: suspend (header: String) -> - ApiResponse, - ): T = withContext(dispatchers.io) { - performRequest(userWalletId, requestBlock = requestBlock) - // to keep behaviour as previous - .fold( - ifRight = { it }, - ifLeft = { error -> error("Cannot perform request: $error") }, - ) - } - suspend fun performWithStaticToken( requestBlock: suspend (header: String) -> ApiResponse, ): Either = withContext(dispatchers.io) { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 78d8486b32..6cf5c28a6f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -17,10 +17,6 @@ import timber.log.Timber private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" -/** - * Returns tangem pay customer info for the main screen banner - * Works only if the user already authorised at least once (won't emit anything otherwise) - */ class TangemPayMainScreenCustomerInfoUseCase( private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt index 405a45ad13..6d9f9d47f2 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt @@ -5,9 +5,13 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent +import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.TangemPayChangePinUM import com.tangem.features.tangempay.model.transformers.PinCodeChangeTransformer import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute @@ -23,6 +27,7 @@ import javax.inject.Inject internal class TangemPayChangePinModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val uiMessageSender: UiMessageSender, private val router: Router, private val cardDetailsRepository: TangemPayCardDetailsRepository, ) : Model() { @@ -49,8 +54,12 @@ internal class TangemPayChangePinModel @Inject constructor( } uiState.update { it.copy(submitButtonLoading = false) } when (result) { + SetPinResult.PIN_TOO_WEAK -> { + uiMessageSender.send( + message = ToastMessage(resourceReference(R.string.tangempay_pin_validation_error_message)), + ) + } SetPinResult.SUCCESS -> router.push(TangemPayDetailsInnerRoute.ChangePINSuccess) - SetPinResult.PIN_TOO_WEAK, SetPinResult.DECRYPTION_ERROR, SetPinResult.UNKNOWN_ERROR, null, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt index eacc9656fd..abf2326617 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt @@ -4,11 +4,11 @@ import com.tangem.core.ui.R 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.messageBottomSheetUM import com.tangem.core.ui.components.bottomsheets.message.onClick import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.features.tangempay.entity.TangemPayViewPinUM import com.tangem.utils.transformer.Transformer @@ -16,7 +16,7 @@ internal class TangemPayViewPinErrorStateTransformer : Transformer Date: Fri, 26 Dec 2025 15:54:17 +0500 Subject: [PATCH 123/130] Updated on 2026-08-14 --- .../tangem/data/pay/repository/DefaultOnboardingRepository.kt | 4 ++-- .../com/tangem/domain/pay/repository/OnboardingRepository.kt | 2 +- .../child/wallet/model/intents/TangemPayClickIntents.kt | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 9b12c88cb6..d877f0f1f3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -224,13 +224,13 @@ internal class DefaultOnboardingRepository @Inject constructor( tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true) } - override suspend fun disableTangemPay(userWalletId: UserWalletId): Either { + override suspend fun disableTangemPay(userWalletId: UserWalletId): Either { return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.setTangemPayEnabledStatus( authHeader = authHeader, body = SetTangemPayEnabledRequest(isTangemPayEnabled = false), ) - }.onRight { + }.map { val address = requestHelper.getCustomerWalletAddress(userWalletId) tangemPayStorage.clearAll(userWalletId = userWalletId, customerWalletAddress = address) setHideMainOnboardingBanner(userWalletId) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 1deca6b43a..568cf85006 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -34,5 +34,5 @@ interface OnboardingRepository { suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) - suspend fun disableTangemPay(userWalletId: UserWalletId): Either + suspend fun disableTangemPay(userWalletId: UserWalletId): Either } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 4664d50c5f..e7a6f9325e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.ToastMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -213,6 +214,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( modelScope.launch { tangemPayOnboardingRepository.disableTangemPay(userWalletId) .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } + .onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) } } } } \ No newline at end of file From 65f026b993af6a8fc5dc8fa4cc0c4e41f134f5ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Dec 2025 15:35:20 +0300 Subject: [PATCH 124/130] Updated on 2026-08-14 --- .../tokenlist/impl/ui/components/MarketsListLazyColumn.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index b74e9a3d29..bbdcbf2697 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -174,6 +175,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod onClick = onShowTokensClick, ), ) + SpacerH12() } } From ce37036dd56689adc6e608477664c5a31f6ec116 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Dec 2025 16:59:20 +0300 Subject: [PATCH 125/130] Updated on 2026-08-14 --- .../CreateHardwareWalletModel.kt | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) 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 0373179468..39a2c8b242 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 @@ -21,6 +21,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.builder.ColdUserWalletBuilder @@ -133,19 +134,16 @@ internal class CreateHardwareWalletModel @Inject constructor( } saveWalletUseCase(userWallet = userWallet).fold( - ifLeft = { + ifLeft = { saveWalletError -> 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) - } - } + when (saveWalletError) { + is SaveWalletError.DataError -> Timber.e(saveWalletError.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> handleAlreadySavedCard( + saveWalletError.messageId, + walletId = userWallet.walletId, + scanResponse = scanResponse, + ) } }, ifRight = { @@ -175,4 +173,18 @@ internal class CreateHardwareWalletModel @Inject constructor( ), ) } + + private suspend fun handleAlreadySavedCard(messageId: Int, walletId: UserWalletId, scanResponse: ScanResponse) { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(messageId), + ), + ) + userWalletsListRepository.unlock( + userWalletId = walletId, + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + ).onRight { + router.replaceAll(AppRoute.Wallet) + } + } } \ No newline at end of file From c0f4f15b5da25cce6a48d7c98f32420541f3fdeb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Dec 2025 11:13:12 +0300 Subject: [PATCH 126/130] Updated on 2026-08-14 --- .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 6 ++++++ .../wallet/analytics/utils/TokenListAnalyticsSender.kt | 1 + 2 files changed, 7 insertions(+) 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 c54ebcef16..033a18e216 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 @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerOnlyEvent import com.tangem.core.analytics.models.OneTimeAnalyticsEvent import com.tangem.domain.models.wallet.UserWalletId @@ -22,6 +23,11 @@ sealed class WalletScreenAnalyticsEvent { override val oneTimeEventId: String = id + userWalletId.stringValue } + class AppsFlyerWalletFunded(userWalletId: UserWalletId) : Basic(event = "wallet_funded"), + AppsFlyerOnlyEvent, OneTimeAnalyticsEvent { + override val oneTimeEventId: String = id + userWalletId.stringValue + } + class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic( event = "Card Was Scanned", params = mapOf( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index b8d4f81c17..215aa3ca8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -206,6 +206,7 @@ internal class TokenListAnalyticsSender @Inject constructor( } analyticsEventHandler.send(Basic.WalletToppedUp(userWallet.walletId, walletType)) + analyticsEventHandler.send(Basic.AppsFlyerWalletFunded(userWallet.walletId)) } } From 7a08266a066c7eef123ff22c56b569751744d451 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Dec 2025 10:27:08 +0300 Subject: [PATCH 127/130] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 4 ++-- .../model/WalletsUpdateActionResolver.kt | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 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 8370377f18..ba456f80f5 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 @@ -679,7 +679,7 @@ internal class WalletModel @Inject constructor( } private suspend fun unlockWallet(action: WalletsUpdateActionResolver.Action.UnlockWallet) { - withContext(dispatchers.io) { delay(timeMillis = 700) } + delay(timeMillis = 700) stateHolder.update( transformer = UnlockWalletTransformer( @@ -696,7 +696,7 @@ internal class WalletModel @Inject constructor( ) action.unlockedWallets.onEach { userWallet -> - modelScope.launch { fetchWalletContent(userWallet = userWallet) } + fetchWalletContent(userWallet = userWallet) } } 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 d498e480a0..4946ef99e9 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,6 +64,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( selectedWallet: UserWallet, ): Action { return when { + isAnyWalletUnlocked(state, wallets) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + isAnyWalletNameChanged(state, wallets) -> { + getRenameWalletsAction(state, wallets) + } isAnyHotWalletUpgraded(state, wallets) -> { getHotWalletsUpgradedAction(state, wallets, selectedWallet) } @@ -79,15 +88,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( selectedWallet = selectedWallet, ) } - isAnyWalletNameChanged(state, wallets) -> { - getRenameWalletsAction(state, wallets) - } - isAnyWalletUnlocked(state, wallets) -> { - Action.UnlockWallet( - selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), - ) - } isSelectedWalletCardsCountChanged(state, selectedWallet) -> { Action.UpdateWalletCardCount(selectedWallet) } From 7e7a07605ae8d833b6a59460dadb4bb78ff470b7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 17:54:01 +0500 Subject: [PATCH 128/130] Updated on 2026-08-14 --- .../cardsettings/model/CardSettingsModel.kt | 24 +++--- .../details/ui/resetcard/ResetCardScreen.kt | 36 ++++----- .../ui/resetcard/ResetCardScreenState.kt | 19 +++-- .../ui/resetcard/api/ResetCardComponent.kt | 1 + .../ui/resetcard/model/ResetCardModel.kt | 73 +++++++++++-------- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + core/res/src/main/res/values/strings.xml | 2 + 8 files changed, 87 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 08aa5ef0b0..797ef6c056 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -54,6 +55,7 @@ internal class CardSettingsModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, + private val onboardingRepository: OnboardingRepository, ) : Model() { private val params = paramsContainer.require() @@ -218,15 +220,19 @@ internal class CardSettingsModel @Inject constructor( } else { val card = scanResponse.card - store.dispatchNavigationAction { - push( - route = AppRoute.ResetToFactory( - userWalletId = userWalletId, - cardId = card.cardId, - isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = scanResponse.getBackupCardsCount() ?: 0, - ), - ) + modelScope.launch { + val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true + store.dispatchNavigationAction { + push( + route = AppRoute.ResetToFactory( + userWalletId = userWalletId, + cardId = card.cardId, + isActiveBackupStatus = card.backupStatus?.isActive == true, + backupCardsCount = scanResponse.getBackupCardsCount() ?: 0, + hasTangemPay = hasTangemPay, + ), + ) + } } } } 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 3e04e9381b..818620b38f 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 @@ -22,6 +22,7 @@ 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 +import kotlinx.collections.immutable.persistentListOf import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog @Composable @@ -114,22 +115,11 @@ private fun Description(text: TextReference) { @Composable private fun Conditions(state: ResetCardScreenState) { state.warningsToShow.forEach { warning -> - when (warning) { - ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { - ConditionCheckBox( - checkedState = state.isAcceptCondition1Checked, - onCheckedChange = state.onAcceptCondition1ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_1), - ) - } - ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { - ConditionCheckBox( - checkedState = state.isAcceptCondition2Checked, - onCheckedChange = state.onAcceptCondition2ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_2), - ) - } - } + ConditionCheckBox( + checkedState = warning.isChecked, + onCheckedChange = { state.onToggleWarning(warning.type) }, + description = warning.description, + ) } } @@ -239,14 +229,16 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { ResetCardScreen( state = ResetCardScreenState( isResetButtonEnabled = true, - isResetPasswordButtonShown = false, - warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS), + warningsToShow = persistentListOf( + ResetCardScreenState.WarningUM( + isChecked = false, + type = ResetCardScreenState.WarningType.LOST_WALLET_ACCESS, + description = TextReference.Res(id = R.string.reset_card_to_factory_condition_1), + ), + ), descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message), - isAcceptCondition1Checked = false, - isAcceptCondition2Checked = false, - onAcceptCondition1ToggleClick = {}, - onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, + onToggleWarning = {}, dialog = null, ), onBackClick = {}, 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 68b8585c72..0011e5dcd6 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 @@ -3,16 +3,13 @@ package com.tangem.tap.features.details.ui.resetcard import androidx.annotation.StringRes import com.tangem.core.ui.extensions.TextReference import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList internal data class ResetCardScreenState( val isResetButtonEnabled: Boolean, val descriptionText: TextReference, - val warningsToShow: List, - val isResetPasswordButtonShown: Boolean, - val isAcceptCondition1Checked: Boolean, - val isAcceptCondition2Checked: Boolean, - val onAcceptCondition1ToggleClick: (Boolean) -> Unit, - val onAcceptCondition2ToggleClick: (Boolean) -> Unit, + val warningsToShow: ImmutableList, + val onToggleWarning: (WarningType) -> Unit, val onResetButtonClick: () -> Unit, val dialog: Dialog?, ) { @@ -58,7 +55,13 @@ internal data class ResetCardScreenState( } } - internal enum class WarningsToReset { - LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE + internal data class WarningUM( + val isChecked: Boolean, + val type: WarningType, + val description: TextReference, + ) + + internal enum class WarningType { + LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE, LOST_TANGEM_PAY } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/api/ResetCardComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/api/ResetCardComponent.kt index 64dc8b1c5d..9f24f61f9b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/api/ResetCardComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/api/ResetCardComponent.kt @@ -11,6 +11,7 @@ interface ResetCardComponent : ComposableContentComponent { val cardId: String, val isActiveBackupStatus: Boolean, val backupCardsCount: Int, + val hasTangemPay: Boolean, ) interface Factory : ComponentFactory diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index eaa4ce0a9f..c53ead9529 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.ResetCardUserCodeParams @@ -30,6 +31,9 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE +import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -82,33 +86,22 @@ internal class ResetCardModel @Inject constructor( // TODO: move logic to separate domain entity private var resetBackupCardCount = 0 + private var warningsMap = emptyMap() + val screenState: MutableStateFlow = MutableStateFlow( value = getInitialState(), ) private fun getInitialState(): ResetCardScreenState { - val shouldShowResetPasswordButton = shouldShowResetPasswordButton() - val warningsToShow = buildList { - add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS) - - if (shouldShowResetPasswordButton) { - add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE) - } - } - return ResetCardScreenState( isResetButtonEnabled = false, descriptionText = getResetToFactoryDescription( isActiveBackupStatus = isActiveBackupPrimaryCard, typesResolver = currentCardTypesResolver, ), - warningsToShow = warningsToShow, - isResetPasswordButtonShown = shouldShowResetPasswordButton, - isAcceptCondition1Checked = false, - isAcceptCondition2Checked = false, - onAcceptCondition1ToggleClick = ::toggleFirstCondition, - onAcceptCondition2ToggleClick = ::toggleSecondCondition, + warningsToShow = buildInitialItems(), onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) }, + onToggleWarning = ::toggleCondition, dialog = null, ) } @@ -119,28 +112,46 @@ internal class ResetCardModel @Inject constructor( return isTangemWallet && isActiveBackupPrimaryCard } - private fun toggleFirstCondition(isAccepted: Boolean) { - screenState.update { prevState -> - val isResetButtonEnabled = if (prevState.isResetPasswordButtonShown) { - isAccepted && prevState.isAcceptCondition2Checked - } else { - isAccepted - } + private fun buildInitialItems(): ImmutableList { + val shouldShowResetPasswordButton = shouldShowResetPasswordButton() + val shouldShowResetTangemPayButton = params.hasTangemPay - prevState.copy( - isAcceptCondition1Checked = isAccepted, - isResetButtonEnabled = isResetButtonEnabled, - ) + val lostWalletUM = ResetCardScreenState.WarningUM( + isChecked = false, + type = ResetCardScreenState.WarningType.LOST_WALLET_ACCESS, + description = TextReference.Res(id = R.string.reset_card_to_factory_condition_1), + ) + val lostPasswordUM = ResetCardScreenState.WarningUM( + isChecked = false, + type = ResetCardScreenState.WarningType.LOST_PASSWORD_RESTORE, + description = TextReference.Res(R.string.reset_card_to_factory_condition_2), + ) + val lostTangemPayUM = ResetCardScreenState.WarningUM( + isChecked = false, + type = ResetCardScreenState.WarningType.LOST_TANGEM_PAY, + description = TextReference.Res(R.string.reset_card_to_factory_condition_3), + ) + + warningsMap = buildMap { + put(ResetCardScreenState.WarningType.LOST_WALLET_ACCESS, lostWalletUM) + if (shouldShowResetPasswordButton) { + put(ResetCardScreenState.WarningType.LOST_PASSWORD_RESTORE, lostPasswordUM) + } + if (shouldShowResetTangemPayButton) { + put(ResetCardScreenState.WarningType.LOST_TANGEM_PAY, lostTangemPayUM) + } } + return warningsMap.values.toImmutableList() } - private fun toggleSecondCondition(isAccepted: Boolean) { + private fun toggleCondition(type: ResetCardScreenState.WarningType) { screenState.update { prevState -> - val isResetButtonEnabled = prevState.isAcceptCondition1Checked && isAccepted - + warningsMap[type]?.let { current -> + warningsMap = warningsMap + (type to current.copy(isChecked = !current.isChecked)) + } prevState.copy( - isAcceptCondition2Checked = isAccepted, - isResetButtonEnabled = isResetButtonEnabled, + warningsToShow = warningsMap.values.toImmutableList(), + isResetButtonEnabled = warningsMap.values.all { it.isChecked }, ) } } 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 715bf51463..19f07cabf5 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 @@ -424,6 +424,7 @@ internal class ChildFactory @Inject constructor( cardId = route.cardId, isActiveBackupStatus = route.isActiveBackupStatus, backupCardsCount = route.backupCardsCount, + hasTangemPay = route.hasTangemPay, ), componentFactory = resetCardComponentFactory, ) 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 c2d8b480d7..e5bdf4dad1 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 @@ -112,6 +112,7 @@ sealed class AppRoute(val path: String) : Route { val cardId: String, val isActiveBackupStatus: Boolean, val backupCardsCount: Int, + val hasTangemPay: Boolean, ) : AppRoute( path = "/reset_to_factory" + "/${userWalletId.stringValue}" + diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1a5e95cd53..9bc5664570 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -816,6 +816,7 @@ Quick recap Related News + Related tokens Sources Stay in the loop NFC is not available on your device @@ -1531,6 +1532,7 @@ We’re fixing a technical issue. Please try again later. Service temporarily unavailable Unable to display details. However, card payments are still working. + Set \nPIN code Not synced Restore access Use USDC for everyday payments From 3ef88c33eecb83f5b5111fe8de0650935982bd18 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Dec 2025 16:06:50 +0300 Subject: [PATCH 129/130] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 55284d02b1..21f77fa638 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1331" +tangemBlockchainSdk = "releases-5.32-1335" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.32-574" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8d813113bdb8ea1adc3a51248928176cc3fbfe9d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Dec 2025 21:47:19 +0500 Subject: [PATCH 130/130] Updated on 2026-08-14 --- .../tap/domain/sdk/impl/DefaultTangemSdkManager.kt | 12 +++++++----- .../tap/domain/sdk/impl/MockTangemSdkManager.kt | 4 ++-- .../TangemPayGenerateAddressAndSignChallengeTask.kt | 4 +--- .../tasks/visa/TangemPaySignWithdrawalHashTask.kt | 10 +--------- data/visa/build.gradle.kts | 1 + .../pay/datasource/DefaultTangemPayAuthDataSource.kt | 11 +++++++++-- .../kotlin/com/tangem/sdk/api/TangemSdkManager.kt | 9 +++++++-- 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 981b99ecf8..ef82b7c01f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -519,13 +519,14 @@ internal class DefaultTangemSdkManager( } override suspend fun tangemPayProduceInitialCredentials( - cardId: String, + preflightReadFilter: PreflightReadFilter, ): Either { return coroutineScope { val result = runTaskAsyncReturnOnMain( runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), - cardId = cardId, + cardId = null, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + preflightReadFilter = preflightReadFilter, ) return@coroutineScope when (result) { @@ -536,14 +537,15 @@ internal class DefaultTangemSdkManager( } override suspend fun getWithdrawalSignature( - cardId: String, hash: String, + preflightReadFilter: PreflightReadFilter, ): Either { return coroutineScope { val result = runTaskAsyncReturnOnMain( - runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()), - cardId = cardId, + runnable = TangemPaySignWithdrawalHashTask(hash = hash.hexToBytes()), + cardId = null, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + preflightReadFilter = preflightReadFilter, ) return@coroutineScope when (result) { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 54e9e33aca..00e4ea971e 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -219,14 +219,14 @@ class MockTangemSdkManager( } override suspend fun tangemPayProduceInitialCredentials( - cardId: String, + preflightReadFilter: PreflightReadFilter, ): Either { error("Not implemented") } override suspend fun getWithdrawalSignature( - cardId: String, hash: String, + preflightReadFilter: PreflightReadFilter, ): Either { error("Not implemented") } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt index 4550d3dc52..c4e437b4ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -62,7 +62,6 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge) val approveResult = runVisaCustomerWalletApproveTask( session = session, - cardId = card.cardId, targetAddress = address, dataToSign = dataToSign, ) @@ -103,14 +102,13 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( private suspend fun runVisaCustomerWalletApproveTask( session: CardSession, - cardId: String, targetAddress: String, dataToSign: VisaDataToSignByCustomerWallet, ): CompletionResult { val deferred = CompletableDeferred>() val task = VisaCustomerWalletApproveTask( visaDataForApprove = VisaCustomerWalletApproveTask.Input( - cardId = cardId, + cardId = null, targetAddress = targetAddress, hashToSign = dataToSign.hashToSign, sign = dataToSign::sign, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt index 2cf1b0f42c..150c5c35ba 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt @@ -14,10 +14,7 @@ import com.tangem.domain.visa.error.VisaActivationError import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand -class TangemPaySignWithdrawalHashTask( - private val cardId: String, - private val hash: ByteArray, -) : CardSessionRunnable { +class TangemPaySignWithdrawalHashTask(private val hash: ByteArray) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { val card = session.environment.card ?: run { @@ -25,11 +22,6 @@ class TangemPaySignWithdrawalHashTask( return } - if (card.cardId != cardId) { - callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) - return - } - proceedSign(card, session, callback) } diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index e1e1568878..3c9ef9ff07 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.error.ext) implementation(projects.core.security) implementation(projects.data.common) + implementation(projects.data.wallets) /** Project - Domain */ implementation(projects.domain.visa) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 6af402c591..8198c181ce 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -1,6 +1,7 @@ package com.tangem.data.pay.datasource import arrow.core.Either +import com.tangem.data.wallets.cold.UserWalletIdPreflightReadFilter import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource @@ -17,7 +18,10 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( userWallet: UserWallet, ): Either { return when (userWallet) { - is UserWallet.Cold -> tangemSdkManager.tangemPayProduceInitialCredentials(cardId = userWallet.cardId) + is UserWallet.Cold -> { + val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId) + tangemSdkManager.tangemPayProduceInitialCredentials(preflightReadFilter = preflightReadFilter) + } is UserWallet.Hot -> tangemPayHotSdkManager.produceInitialCredentials(userWallet) } } @@ -27,7 +31,10 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( hash: String, ): Either { return when (userWallet) { - is UserWallet.Cold -> tangemSdkManager.getWithdrawalSignature(cardId = userWallet.cardId, hash = hash) + is UserWallet.Cold -> { + val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId) + tangemSdkManager.getWithdrawalSignature(hash = hash, preflightReadFilter = preflightReadFilter) + } is UserWallet.Hot -> tangemPayHotSdkManager.getWithdrawalSignature(hotWallet = userWallet, hash = hash) } } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 07a2d02773..64016c2cf8 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -161,8 +161,13 @@ interface TangemSdkManager { visaDataForApprove: VisaDataForApprove, ): CompletionResult - suspend fun tangemPayProduceInitialCredentials(cardId: String): Either + suspend fun tangemPayProduceInitialCredentials( + preflightReadFilter: PreflightReadFilter, + ): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): Either + suspend fun getWithdrawalSignature( + hash: String, + preflightReadFilter: PreflightReadFilter, + ): Either // endregion } \ No newline at end of file