From e3ea28584bf9c7275dfb8c29333d0edf5c6e4124 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Nov 2025 18:11:10 +0200 Subject: [PATCH 001/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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 From 0d683cbd459e674bf83640386bf74a75c0b2046b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Dec 2025 07:55:03 +0000 Subject: [PATCH 131/154] 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 3a00dc0651..21f77fa638 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-1334" +tangemBlockchainSdk = "releases-5.32-1335" #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 43b4354719f81a04e1198d1b64c3e614f54bf2da Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Dec 2025 15:05:21 +0300 Subject: [PATCH 132/154] Updated on 2026-08-14 --- .../features/biometry/impl/model/AskBiometryModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 2d586fad73..1f8093ed68 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 @@ -116,9 +116,11 @@ internal class AskBiometryModel @Inject constructor( walletsRepository.setUseBiometricAuthentication(value = true) walletsRepository.setRequireAccessCode(value = false) setBiometryLockForAllWallets() - cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), - ) + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + } } else { settingsRepository.setShouldSaveAccessCodes(value = true) if (userWallet is UserWallet.Cold) { From 4691835739c095b1ff024fa507f0d50b5f415741 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Dec 2025 16:44:56 +0300 Subject: [PATCH 133/154] Updated on 2026-08-14 --- .../handlers/appsflyer/AppsFlyerAnalyticsClient.kt | 14 +++++++++++--- .../appsflyer/AppsFlyerAnalyticsHandler.kt | 12 +++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) 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 755f393182..7fab19081f 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 @@ -2,16 +2,17 @@ package com.tangem.tap.common.analytics.handlers.appsflyer import android.content.Context import com.appsflyer.AppsFlyerLib +import com.appsflyer.attribution.AppsFlyerRequestListener import com.tangem.core.analytics.api.EventLogger import com.tangem.core.analytics.api.UserIdHolder import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter +import timber.log.Timber interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder internal class AppsFlyerClient( private val context: Context, key: String, - appId: String, ) : AppsFlyerAnalyticsClient { private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() @@ -19,8 +20,15 @@ internal class AppsFlyerClient( init { appsFlyerLib.init(key, null, context) - appsFlyerLib.setAppId(appId) - appsFlyerLib.start(context) + appsFlyerLib.start(context, key, object : AppsFlyerRequestListener { + override fun onSuccess() { + Timber.d("AppsFlyer initialized successfully") + } + + override fun onError(p0: Int, p1: String) { + Timber.e("AppsFlyer initialization error: $p0, $p1") + } + }) } override fun setUserId(userId: String) { 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 d9f80b3476..cda56807ee 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 @@ -40,10 +40,12 @@ class AppsFlyerAnalyticsHandler( } class Builder : AnalyticsHandlerBuilder { - 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) } + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler( + client = if (data.logConfig.isAppsflyerLogEnabled) { + AppsFlyerLogClient(data.jsonConverter) + } else { + AppsFlyerClient(data.application, data.config.appsFlyerApiKey) + }, + ) } } \ No newline at end of file From 03cdfcfede22a32ba2790301acb5a04e54abf681 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Dec 2025 15:28:15 +0500 Subject: [PATCH 134/154] Updated on 2026-08-14 --- .../com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7ccc87275c..9c27024b35 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 @@ -10,6 +10,7 @@ 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.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest @@ -87,7 +88,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( private fun UserWallet.isCompatible(): Boolean = when (this) { is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable - is UserWallet.Hot -> true + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } private suspend fun List.addPaeraCustomersData(): List { From 52311652f1da61177a5e3e343cdf14ba221a9567 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Jan 2026 14:00:26 +0500 Subject: [PATCH 135/154] Updated on 2026-08-14 --- .../singlecurrency/TangemPayCardMainBlock.kt | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt index 0c7551a2b4..c155df8fd4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface @@ -9,13 +10,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.orMaskWithStars 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 import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock @Composable internal fun TangemPayCardMainBlock( @@ -33,8 +38,9 @@ internal fun TangemPayCardMainBlock( modifier = Modifier .fillMaxWidth() .height(IntrinsicSize.Min) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = 12.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Image( painter = painterResource(R.drawable.img_visa_36), @@ -42,20 +48,15 @@ internal fun TangemPayCardMainBlock( modifier = Modifier.size(36.dp), ) - Spacer(Modifier.width(12.dp)) - Column( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = stringResourceSafe(R.string.tangempay_payment_account), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, ) - - Spacer(Modifier.height(6.dp)) - Text( text = state.lastFourDigits.resolveReference(), style = TangemTheme.typography.caption2, @@ -64,6 +65,7 @@ internal fun TangemPayCardMainBlock( } Column( modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), horizontalAlignment = Alignment.End, ) { Text( @@ -72,14 +74,30 @@ internal fun TangemPayCardMainBlock( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.End, ) - Text( text = state.balanceSymbol.resolveReference(), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.End, ) } } } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayCardMainBlockPreview() { + TangemThemePreview { + TangemPayMainScreenBlock( + TangemPayState.Card( + lastFourDigits = TextReference.Str("*1234"), + balanceText = TextReference.Str("$ 0.00"), + balanceSymbol = TextReference.Str("USDC"), + onClick = {}, + ), + isBalanceHidden = false, + ) + } } \ No newline at end of file From 80d66f252ec14aeae394dbce224c383bab9a915f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Jan 2026 19:57:31 +0300 Subject: [PATCH 136/154] Updated on 2026-08-14 --- .../appsflyer/AppsFlyerAnalyticsClient.kt | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) 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 7fab19081f..4638a88856 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 @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics.handlers.appsflyer import android.content.Context +import com.appsflyer.AppsFlyerConversionListener import com.appsflyer.AppsFlyerLib import com.appsflyer.attribution.AppsFlyerRequestListener import com.tangem.core.analytics.api.EventLogger @@ -17,18 +18,39 @@ internal class AppsFlyerClient( private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() private val eventConverter = UnderscoreAnalyticsEventConverter() + private val logEventListener = object : AppsFlyerRequestListener { + override fun onSuccess() { + Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send") + } + + override fun onError(p0: Int, p1: String) { + Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1") + } + } init { - appsFlyerLib.init(key, null, context) - appsFlyerLib.start(context, key, object : AppsFlyerRequestListener { - override fun onSuccess() { - Timber.d("AppsFlyer initialized successfully") - } + val conversionListener = object : AppsFlyerConversionListener { + override fun onConversionDataSuccess(p0: Map?) {} + override fun onConversionDataFail(p0: String?) {} + override fun onAppOpenAttribution(p0: Map?) {} + override fun onAttributionFailure(p0: String?) {} + } - override fun onError(p0: Int, p1: String) { - Timber.e("AppsFlyer initialization error: $p0, $p1") - } - }) + appsFlyerLib.run { + setAppId(context.packageName) + setDebugLog(true) + + init(key, conversionListener, context) + start(context, key, object : AppsFlyerRequestListener { + override fun onSuccess() { + Timber.i("AppsFlyer initialized successfully") + } + + override fun onError(p0: Int, p1: String) { + Timber.e("AppsFlyer initialization error: $p0, $p1") + } + }) + } } override fun setUserId(userId: String) { @@ -44,6 +66,7 @@ internal class AppsFlyerClient( context, event, eventConverter.convertEventParams(params), + logEventListener, ) } } \ No newline at end of file From c703edde8ee28a896cf3b98c7036b21a1ecc7381 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Jan 2026 07:31:15 +0300 Subject: [PATCH 137/154] Updated on 2026-08-14 --- .../appsflyer/AppsFlyerAnalyticsClient.kt | 19 +++++++++++++++---- .../appsflyer/AppsFlyerAnalyticsHandler.kt | 13 +++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) 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 4638a88856..231660ff27 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 @@ -30,10 +30,18 @@ internal class AppsFlyerClient( init { val conversionListener = object : AppsFlyerConversionListener { - override fun onConversionDataSuccess(p0: Map?) {} - override fun onConversionDataFail(p0: String?) {} - override fun onAppOpenAttribution(p0: Map?) {} - override fun onAttributionFailure(p0: String?) {} + override fun onConversionDataSuccess(p0: Map?) { + Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}") + } + override fun onConversionDataFail(p0: String?) { + Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}") + } + override fun onAppOpenAttribution(p0: Map?) { + Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}") + } + override fun onAttributionFailure(p0: String?) { + Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}") + } } appsFlyerLib.run { @@ -41,6 +49,7 @@ internal class AppsFlyerClient( setDebugLog(true) init(key, conversionListener, context) + Timber.i("Starting AppsFlyer SDK") start(context, key, object : AppsFlyerRequestListener { override fun onSuccess() { Timber.i("AppsFlyer initialized successfully") @@ -50,6 +59,7 @@ internal class AppsFlyerClient( Timber.e("AppsFlyer initialization error: $p0, $p1") } }) + Timber.i("AppsFlyer SDK started") } } @@ -62,6 +72,7 @@ internal class AppsFlyerClient( } override fun logEvent(event: String, params: Map) { + Timber.tag("AppsFlyer").i("Logging event: $event with params: $params") appsFlyerLib.logEvent( context, event, 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 cda56807ee..14a35c89cd 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 @@ -6,19 +6,26 @@ 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 +import timber.log.Timber class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, ) : AnalyticsHandler, AnalyticsUserIdHandler { + init { + Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler created") + } + override fun id(): String = ID override fun send(event: AnalyticsEvent) { when (event) { is AppsFlyerOnlyEvent -> { + Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") client.logEvent(event.id, event.params) } is AppsFlyerIncludedEvent -> { + Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") client.logEvent( event = AnalyticsEvent(category = event.category, event = event.appsFlyerReplacedEvent).id, params = event.params, @@ -40,10 +47,16 @@ class AppsFlyerAnalyticsHandler( } class Builder : AnalyticsHandlerBuilder { + init { + Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created") + } + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler( client = if (data.logConfig.isAppsflyerLogEnabled) { + Timber.tag("AppsFlyer").i("AppsFlyer log enabled, mock client created") AppsFlyerLogClient(data.jsonConverter) } else { + Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created") AppsFlyerClient(data.application, data.config.appsFlyerApiKey) }, ) From 8e77f5b2d3c79352f17a8656e9a83f0625a10c84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Jan 2026 14:17:00 +0500 Subject: [PATCH 138/154] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 20 ------------- .../data/wallets/DefaultWalletsRepository.kt | 13 --------- .../wallets/repository/WalletsRepository.kt | 4 --- ...DismissUpgradeWalletNotificationUseCase.kt | 12 -------- ...UpgradeWalletNotificationEnabledUseCase.kt | 13 --------- .../hotwallet/accesscode/ui/AccessCode.kt | 2 +- .../forgetwallet/ForgetWalletModel.kt | 28 ++++++------------- .../forgetwallet/entity/ForgetWalletUM.kt | 6 ++-- .../forgetwallet/ui/ForgetWalletContent.kt | 17 +++-------- .../preview/PreviewWalletSettingsComponent.kt | 2 -- .../entity/WalletSettingsItemUM.kt | 1 - .../model/WalletSettingsModel.kt | 19 ++----------- .../walletsettings/ui/WalletSettingsScreen.kt | 2 -- .../walletsettings/utils/ItemsBuilder.kt | 27 ++++++------------ .../welcome/impl/ui/WelcomeSelectWallet.kt | 2 +- 15 files changed, 27 insertions(+), 141 deletions(-) delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index f6d5141d1e..374af96dd0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -396,26 +396,6 @@ internal object WalletsDomainModule { ) } - @Provides - @Singleton - fun providesIsUpgradeWalletNotificationEnabledUseCase( - walletsRepository: WalletsRepository, - ): IsUpgradeWalletNotificationEnabledUseCase { - return IsUpgradeWalletNotificationEnabledUseCase( - walletsRepository = walletsRepository, - ) - } - - @Provides - @Singleton - fun providesDismissUpgradeWalletNotificationUseCase( - walletsRepository: WalletsRepository, - ): DismissUpgradeWalletNotificationUseCase { - return DismissUpgradeWalletNotificationUseCase( - walletsRepository = walletsRepository, - ) - } - @Provides @Singleton fun providesUnlockHotWalletContextualUseCase( 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 45b444d02f..def9a96402 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 @@ -50,9 +50,6 @@ internal class DefaultWalletsRepository( private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { - 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) @@ -339,16 +336,6 @@ internal class DefaultWalletsRepository( } } - override fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow { - return upgradeWalletNotificationDisabled.map { - it.contains(userWalletId) - } - } - - override suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) { - upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } - } - override suspend fun setWalletName(walletId: UserWalletId, walletName: String) = withContext(dispatchers.io) { val userWallet = userWalletsStore.getSyncOrNull(key = walletId) 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 430653e85c..1431dc5e6f 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 @@ -60,10 +60,6 @@ interface WalletsRepository { suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) - fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow - - suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) - @Throws suspend fun setWalletName(walletId: UserWalletId, walletName: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt deleted file mode 100644 index 2aeb755fba..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.repository.WalletsRepository - -class DismissUpgradeWalletNotificationUseCase( - private val walletsRepository: WalletsRepository, -) { - suspend operator fun invoke(userWalletId: UserWalletId) { - walletsRepository.dismissUpgradeWalletNotification(userWalletId) - } -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt deleted file mode 100644 index d38fec4fe2..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.repository.WalletsRepository -import kotlinx.coroutines.flow.Flow - -class IsUpgradeWalletNotificationEnabledUseCase( - private val walletsRepository: WalletsRepository, -) { - operator fun invoke(userWalletId: UserWalletId): Flow { - return walletsRepository.isUpgradeWalletNotificationEnabled(userWalletId) - } -} \ 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 f92f63bdfe..0be023b18b 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 @@ -104,7 +104,7 @@ internal fun AccessCode( ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = state.isConfirmMode, + isPasswordVisual = true, value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index 88fc9882cd..a4d9c89ffb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -38,31 +38,19 @@ internal class ForgetWalletModel @Inject constructor( field = MutableStateFlow( ForgetWalletUM( onBackClick = { router.pop() }, - firstCheckboxChecked = false, - secondCheckboxChecked = false, - onFirstCheckboxClick = ::onFirstCheckboxClick, - onSecondCheckboxClick = ::onSecondCheckboxClick, + isCheckboxChecked = false, + onCheckboxClick = ::onCheckboxClick, onForgetWalletClick = ::onForgetWalletClick, isForgetButtonEnabled = false, ), ) - private fun onFirstCheckboxClick() { - uiState.update { - val newValue = !it.firstCheckboxChecked - it.copy( - firstCheckboxChecked = newValue, - isForgetButtonEnabled = newValue && it.secondCheckboxChecked, - ) - } - } - - private fun onSecondCheckboxClick() { - uiState.update { - val newValue = !it.secondCheckboxChecked - it.copy( - secondCheckboxChecked = newValue, - isForgetButtonEnabled = it.firstCheckboxChecked && newValue, + private fun onCheckboxClick() { + uiState.update { currentState -> + val newValue = !currentState.isCheckboxChecked + currentState.copy( + isCheckboxChecked = newValue, + isForgetButtonEnabled = newValue, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/entity/ForgetWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/entity/ForgetWalletUM.kt index 0cb0af23d1..f84f77d5c5 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/entity/ForgetWalletUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/entity/ForgetWalletUM.kt @@ -2,10 +2,8 @@ package com.tangem.features.hotwallet.forgetwallet.entity internal data class ForgetWalletUM( val onBackClick: () -> Unit, - val firstCheckboxChecked: Boolean, - val secondCheckboxChecked: Boolean, - val onFirstCheckboxClick: () -> Unit, - val onSecondCheckboxClick: () -> Unit, + val isCheckboxChecked: Boolean, + val onCheckboxClick: () -> Unit, val onForgetWalletClick: () -> Unit, val isForgetButtonEnabled: Boolean, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt index 5e4d2f42ec..5dfc22b13c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ui/ForgetWalletContent.kt @@ -70,15 +70,8 @@ internal fun ForgetWalletContent(state: ForgetWalletUM, modifier: Modifier = Mod Spacer(modifier = Modifier.height(48.dp)) CheckboxItem( modifier = Modifier.padding(horizontal = 16.dp), - checked = state.firstCheckboxChecked, - onCheckedChange = state.onFirstCheckboxClick, - text = stringResourceSafe(R.string.hw_remove_wallet_warning_device), - ) - Spacer(modifier = Modifier.height(16.dp)) - CheckboxItem( - modifier = Modifier.padding(horizontal = 16.dp), - checked = state.secondCheckboxChecked, - onCheckedChange = state.onSecondCheckboxClick, + checked = state.isCheckboxChecked, + onCheckedChange = state.onCheckboxClick, text = stringResourceSafe(R.string.hw_remove_wallet_warning_access), ) Spacer(modifier = Modifier.height(32.dp)) @@ -142,10 +135,8 @@ private fun PreviewForgetWalletContent() { ForgetWalletContent( state = ForgetWalletUM( onBackClick = {}, - firstCheckboxChecked = true, - secondCheckboxChecked = false, - onFirstCheckboxClick = {}, - onSecondCheckboxClick = {}, + isCheckboxChecked = false, + onCheckboxClick = {}, onForgetWalletClick = {}, isForgetButtonEnabled = false, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index bdf3f94838..34449ac010 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -65,9 +65,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onNotificationsDescriptionClick = {}, isNotificationsPermissionGranted = false, onAccessCodeClick = {}, - walletUpgradeDismissed = false, onUpgradeWalletClick = {}, - onDismissUpgradeWalletClick = {}, onBackupClick = {}, onCardSettingsClick = {}, accountsUM = previewAccounts(), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 668efd566b..a495db9417 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -52,7 +52,6 @@ internal sealed class WalletSettingsItemUM { val title: TextReference, val description: TextReference, val onClick: () -> Unit, - val onDismissClick: () -> Unit, ) : WalletSettingsItemUM() } 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 b5ba400298..8e97fb4927 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 @@ -86,8 +86,6 @@ internal class WalletSettingsModel @Inject constructor( private val settingsManager: SettingsManager, private val permissionsRepository: PermissionRepository, private val notificationsRepository: NotificationsRepository, - private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, - private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, @@ -140,10 +138,9 @@ internal class WalletSettingsModel @Inject constructor( flow2 = getWalletNotificationsEnabledUseCase(params.userWalletId) .distinctUntilChanged() .conflate(), - flow3 = isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), - flow4 = walletCardItemDelegate.cardItemFlow(wallet), - flow5 = accountItemsDelegate.loadAccount(wallet), - ) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem, accountList -> + flow3 = walletCardItemDelegate.cardItemFlow(wallet), + flow4 = accountItemsDelegate.loadAccount(wallet), + ) { nftEnabled, notificationsEnabled, cardItem, accountList -> val isWalletBackedUp = when (wallet) { is UserWallet.Hot -> wallet.backedUp is UserWallet.Cold -> true @@ -156,7 +153,6 @@ internal class WalletSettingsModel @Inject constructor( isNFTEnabled = nftEnabled, isNotificationsEnabled = notificationsEnabled, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), - isUpgradeNotificationEnabled = isUpgradeNotificationEnabled, accountList = accountList, ), accountReorderUM = AccountReorderUM( @@ -197,7 +193,6 @@ internal class WalletSettingsModel @Inject constructor( isNFTEnabled: Boolean, isNotificationsEnabled: Boolean, isNotificationsPermissionGranted: Boolean, - isUpgradeNotificationEnabled: Boolean, accountList: List, ): PersistentList { val isAccountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled @@ -254,9 +249,7 @@ internal class WalletSettingsModel @Inject constructor( onCheckedNotificationsChanged = ::onCheckedNotificationsChange, onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, onAccessCodeClick = { onAccessCodeClick(userWallet) }, - walletUpgradeDismissed = isUpgradeNotificationEnabled, onUpgradeWalletClick = { onUpgradeWalletClick() }, - onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, onBackupClick = ::onBackupClick, onCardSettingsClick = ::onCardSettingsClick, accountsUM = accountList, @@ -411,12 +404,6 @@ internal class WalletSettingsModel @Inject constructor( } } - private fun onDismissUpgradeWalletClick() { - modelScope.launch { - dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId) - } - } - private fun onBackupClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup()) router.push(AppRoute.WalletBackup(params.userWalletId)) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 3041a0c1c6..7f9cb9d418 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -281,7 +281,6 @@ private fun UpgradeWalletBlock(model: WalletSettingsItemUM.UpgradeWallet, modifi iconResId = R.drawable.ic_hardware_backup_36, iconSize = 36.dp, onClick = model.onClick, - onCloseClick = model.onDismissClick, shouldShowArrowIcon = false, ), modifier = modifier, @@ -493,7 +492,6 @@ private fun Preview_WalletSettingsScreen1() { title = stringReference("Upgrade wallet with a hardware backup"), description = stringReference("Keep your crypto safe with Tangem’s best-in-class hardware wallet."), onClick = {}, - onDismissClick = {}, ), ) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index ed3c42d51e..86b1637320 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -38,9 +38,7 @@ internal class ItemsBuilder @Inject constructor() { onReferralClick: () -> Unit, onManageTokensClick: () -> Unit, onAccessCodeClick: () -> Unit, - walletUpgradeDismissed: Boolean, onUpgradeWalletClick: () -> Unit, - onDismissUpgradeWalletClick: () -> Unit, onBackupClick: () -> Unit, onCardSettingsClick: () -> Unit, ): PersistentList = persistentListOf() @@ -48,9 +46,7 @@ internal class ItemsBuilder @Inject constructor() { .addAll( buildUpgradeWalletItem( userWallet = userWallet, - walletUpgradeDismissed = walletUpgradeDismissed, onUpgradeWalletClick = onUpgradeWalletClick, - onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, ), ) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) @@ -122,24 +118,17 @@ internal class ItemsBuilder @Inject constructor() { private fun buildUpgradeWalletItem( userWallet: UserWallet, - walletUpgradeDismissed: Boolean, onUpgradeWalletClick: () -> Unit, - onDismissUpgradeWalletClick: () -> Unit, ): List = when (userWallet) { is UserWallet.Cold -> emptyList() - is UserWallet.Hot -> if (!walletUpgradeDismissed) { - listOf( - WalletSettingsItemUM.UpgradeWallet( - id = "upgrade_wallet", - title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), - description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), - onClick = onUpgradeWalletClick, - onDismissClick = onDismissUpgradeWalletClick, - ), - ) - } else { - emptyList() - } + is UserWallet.Hot -> listOf( + WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), + description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), + onClick = onUpgradeWalletClick, + ), + ) } private fun buildNotificationsPermissionItem() = WalletSettingsItemUM.NotificationPermission( diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index 17c774d9ad..0ede259e38 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -83,7 +83,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) if (state.showUnlockWithBiometricButton) { - SecondaryButton( + PrimaryButton( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() From c9962706e325927a95b7f300d8322c48baf24184 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Jan 2026 18:43:48 +0300 Subject: [PATCH 139/154] Updated on 2026-08-14 --- .../tangem/feature/swap/model/SwapModel.kt | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) 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 cbed111a44..38d591dd72 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 @@ -1039,6 +1039,7 @@ internal class SwapModel @Inject constructor( } } + @Suppress("LongMethod") private fun onTokenSelect(id: String) { val tokens = dataState.tokensDataState ?: return val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) @@ -1069,6 +1070,8 @@ internal class SwapModel @Inject constructor( coin = newToken, isFromCurrency = true, ) + } else { + fromTokenBalanceJobHolder.cancel() } } else { fromToken = initialFromStatus @@ -1087,6 +1090,8 @@ internal class SwapModel @Inject constructor( coin = newToken, isFromCurrency = false, ) + } else { + toTokenBalanceJobHolder.cancel() } } @@ -1141,6 +1146,7 @@ internal class SwapModel @Inject constructor( } } + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun subscribeToCoinBalanceUpdates( userWalletId: UserWalletId, coin: CryptoCurrency.Coin, @@ -1163,20 +1169,25 @@ internal class SwapModel @Inject constructor( ).getOrNull() ?: currencyStatus, ) - uiState = if (isFromCurrency) { - dataState = dataState.copy( - fromCryptoCurrency = currencyStatus, - fromAccount = account, - ) - stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) - } else { - dataState = dataState.copy( - toCryptoCurrency = currencyStatus, - toAccount = account, - ) - stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) + uiState = when { + isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { + dataState = dataState.copy( + fromCryptoCurrency = currencyStatus, + fromAccount = account, + ) + stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) + } + !isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> { + dataState = dataState.copy( + toCryptoCurrency = currencyStatus, + toAccount = account, + ) + stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) + } + else -> { + uiState + } } - startLoadingQuotesFromLastState(isSilent = true) } } else { @@ -1196,14 +1207,19 @@ internal class SwapModel @Inject constructor( ).getOrNull() ?: status, ) - uiState = if (isFromCurrency) { - dataState = dataState.copy(fromCryptoCurrency = status) - stateBuilder.updateSendCurrencyBalance(uiState, status) - } else { - dataState = dataState.copy(toCryptoCurrency = status) - stateBuilder.updateReceiveCurrencyBalance(uiState, status) + uiState = when { + isFromCurrency && status.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { + dataState = dataState.copy(fromCryptoCurrency = status) + stateBuilder.updateSendCurrencyBalance(uiState, status) + } + !isFromCurrency && status.currency.id == dataState.toCryptoCurrency?.currency?.id -> { + dataState = dataState.copy(toCryptoCurrency = status) + stateBuilder.updateReceiveCurrencyBalance(uiState, status) + } + else -> { + uiState + } } - startLoadingQuotesFromLastState(isSilent = true) } }.flowOn(dispatchers.main) From af99b446cd9bbc7a50c8b846e719d8449f4ddddf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Jan 2026 11:45:36 +0300 Subject: [PATCH 140/154] 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 21f77fa638..9f09031069 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ 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-539" +tangemHotSdk = "develop-541" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From acdddc026b8569c01d10ee5de55630dce137fe5a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Jan 2026 14:28:54 +0500 Subject: [PATCH 141/154] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 20 +- .../datasource/api/pay/TangemPayAuthApi.kt | 2 + core/res/src/main/res/values-de/strings.xml | 32 +- core/res/src/main/res/values-es/strings.xml | 223 ++++++++- core/res/src/main/res/values-fr/strings.xml | 209 +++++++- core/res/src/main/res/values-it/strings.xml | 3 +- core/res/src/main/res/values-ja/strings.xml | 72 ++- core/res/src/main/res/values-ru/strings.xml | 53 ++- .../src/main/res/values-uk-rUA/strings.xml | 450 +++++++++++++++++- .../src/main/res/values-zh-rTW/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 36 +- .../repository/TangemPayRequestPerformer.kt | 10 +- .../visa/DefaultTangemPayRemoteDataSource.kt | 2 + .../domain/visa/model/TangemPayAuthTokens.kt | 1 + 14 files changed, 1043 insertions(+), 73 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 fb02cff093..4dc9cefd1a 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -16,6 +16,7 @@ import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -71,12 +72,21 @@ internal class DefaultTangemPayStorage @Inject constructor( ) } - override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? = - withContext(dispatcherProvider.io) { - secureStorage.get(createAuthTokensKey(customerWalletAddress)) - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(tokensAdapter::fromJson) + override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? { + val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) + + return authTokens?.let { tokens -> + if (tokens.idempotencyKey == null) { + val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) + storeAuthTokens(customerWalletAddress, newAuthTokens) + newAuthTokens + } else { + tokens + } } + } override suspend fun clearAuthTokens(customerWalletAddress: String) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt index fbf512024c..7cf839ce31 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt @@ -7,6 +7,7 @@ import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessT import com.tangem.datasource.api.pay.models.response.TangemPayGenerateNonceResponse import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse import retrofit2.http.Body +import retrofit2.http.Header import retrofit2.http.POST interface TangemPayAuthApi { @@ -23,6 +24,7 @@ interface TangemPayAuthApi { @POST("auth/token/refresh") suspend fun refreshCustomerWalletAccessToken( + @Header("Idempotency-Key") idempotencyKey: String, @Body request: RefreshCustomerWalletAccessTokenRequest, ): ApiResponse } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 81ee6497c5..db29c44fbe 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -61,8 +61,8 @@ Bist Du sicher, dass Du das neue Konto verwerfen willst? Bist Du sicher, dass Du die Bearbeitungen verwerfen willst? Nicht gespeicherte Änderungen - Einige benutzerdefinierte Token wurden von “%1$s” Zu “%2$s” da ihre Herleitung zu diesem Konto gehört. - Einige benutzerdefinierte Token wurden verschoben. + Einige benutzerdefinierte Token werden automatisch von „%1$s\" Zu \"%2$sda ihre Herleitung zu diesem Konto gehört. + Einige benutzerdefinierte Token werden automatisch verschoben. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu. Verkaufen @@ -182,8 +182,8 @@ Alle Tangem-Geräte wurden zurückgesetzt. Du kannst nun mit dem Upgrade Deiner Wallet fortfahren. Erneut aktualisieren Zurücksetzen abgeschlossen - Du hast nicht all Deine Tangem-Geräte zurückgesetzt. - Du hast nicht alle Deine Tangem-Geräte zurückgesetzt. + Wir empfehlen, den Reset-Prozess für alle Tangem-Geräte in dieser Wallet abzuschließen. + Einige Tangem-Geräte müssen noch zurückgesetzt werden. Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte oder Ring zum Zurücksetzen von Zugangscodes auf anderen Karten oder Ringe in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte oder Ring nicht zurücksetzen kannst. Ermöglicht die Verwendung dieser Karte oder Ring zum Zurücksetzen des Zugangscodes auf anderen Karten oder Ringe in dieser Brieftasche Zugangspin resetten @@ -276,7 +276,7 @@ Aktivieren Aktiviert Fehler - Geschätzte Gebühr + Aufladegebühr Umtausch Erkunden Transaktionsverlauf einsehen @@ -303,7 +303,7 @@ Importieren In Arbeit Später - Mehr erfahren + Mehr erfahren %1$s übrig Legacy Bitcoin Gesperrt @@ -539,6 +539,10 @@ ID: %s Transaktions-ID kopiert Tauschen jeden Vermögenswert in Deinem Portfolio gegen diesen Token + Eine höhere Geschwindigkeit bedeutet eine schnellere Bestätigung und eine höhere Netzgebühr. %s + Geschwindigkeit wählen + Wähle aus, mit welchem Token Du die Netzgebühr bezahlen möchtest. %s + Token auswählen Markt & Nachrichten Aktuelle Trends Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest. @@ -623,9 +627,9 @@ Backup anzeigen Entferne Deine Wallet für immer! Trotzdem entfernen! - 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? + Diese Wallet verfügt über ein Backup. Stelle sicher, dass Du es wiederherstellen kannst, bevor die Wallet resettet wird. + Wenn Du diese Wallet ohne Backup löschst, verlierst Du dauerhaft den Zugriff auf Dein Guthaben. + Bist du sicher, dass du diese Wallet vergessen willst? 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 @@ -1474,7 +1478,7 @@ Tippe auf „Karte hinzufügen“. Tippen Sie auf „Debit- oder Kreditkarte“ Gebe die Kartendaten manuell ein - Verifiziere die Karte mit dem Einmalpasswort (OPT), das an Dein Gerät gesendet wird. + Verifiziere Deine Karte mithilfe des an Dein Gerät gesendeten OTP. Alles erledigt! Deine Karte ist einsatzbereit. Karte zu Google Pay hinzufügen Karte zu Apple Pay hinzufügen @@ -1505,7 +1509,7 @@ Es dauert in der Regel bis zu 15 Minuten. Einrichtung Ihrer Tangem-Karte Ausstellung Deiner Karte - Wir bereiten Ihre Karte vor. Dies kann etwas dauern. + Die Karte wird in der Regel innerhalb von 5 Minuten automatisch ausgestellt. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann die Ausstellung bis zu 48 Stunden dauern. Tangem Pay Konvertierung bestätigen Möchtest Du den KYC wirklich abbrechen? Du kannst später jederzeit weiter machen. @@ -1514,7 +1518,7 @@ KYC in Bearbeitung Status anzeigen KYC für Tangem Pay in Arbeit - Über die Schaltflächen unten kannst Du Deinen aktuellen KYC-Status einsehen oder ihn abbrechen. + Dokumente werden in der Regel innerhalb von 5 Minuten automatisch überprüft. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann es bis zu 48 Stunden dauern. Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Nutzen Sie USDC für alltägliche Zahlungen Karte erhalten @@ -2012,7 +2016,7 @@ Deine Gelder werden derzeit dem Aave-Protokoll bereitgestellt, Du kannst sie jedoch jederzeit verwalten. Deine%s ist in Aave hinterlegt Chart konnte nicht geladen werden... - Das Bereitstellen von %1$s %2$s bei Aave steht aus. + Lieferung %1$s %2$s nach Aave. Deaktiviere den Yield-Modus APY %1$s%% Verfügbar @@ -2078,7 +2082,7 @@ Angebot Effektiver Jahreszins für Versorgung Effektiver Jahreszins - Die Zinsen fallen automatisch an. + Zinsen fallen automatisch an. Zinsen fallen automatisch an Ertragsmodus Bearbeitung Deiner Einzahlung diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1c463c9d8e..70a5296c78 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -16,10 +16,53 @@ Establezca un código de acceso de %sdígitos para desbloquear su billetera. Crear código de acceso Código de acceso + No puede crear más de %1$s cuentas. Archive una para añadir una nueva. + No se puede agregar una nueva cuenta + Cuenta archivada + Cuentas archivadas + Recuperar + Está a punto de recuperar \"%1$s\". + Recuperar cuenta + Ha alcanzado el límite de 20 cuentas activas. Archive una cuenta para recuperar esta. + No se puede recuperar la cuenta + Archivado + No hemos podido archivar la cuenta. Vuelva a intentarlo más tarde. + Esta cuenta está participando en el programa de referidos. + Esta cuenta no se puede archivar + No se ha podido crear la cuenta. Por favor, inténtelo más tarde. + Cuenta creada Archivar cuenta Archivar Estás archivando esta cuenta, pero siempre puede recuperarla. + Archivando... Cuenta + No se puede editar la cuenta + Cuenta guardada + Cuenta para recompensas + Cuenta #%s - utilizada para la derivación de la dirección. + Añadir cuenta + Guardar + Nombre de la cuenta + Ya existe una cuenta con este nombre. Por favor, elija un nombre diferente. + Nombre de cuenta ya utilizado + Cuenta + Nueva cuenta + Añadir cuenta + Editar cuenta + Vuelva a intentarlo más tarde. Si el problema persiste, póngase en contacto con el servicio de asistencia y le ayudaremos a resolverlo. + %1$s en %2$s + Cuenta principal + Ya ha superado el límite de %1$s cuentas activas. Archive una para recuperarla + No se puede recuperar la cuenta + Cuenta recuperada + Pulse prolongadamente sobre una cuenta para reordenarla + Seguir editando + Descartar + ¿Seguro que quieres descartar la cuenta nueva? + ¿Seguro que deseas descartar las ediciones? + Cambios no guardados + Algunos tokens personalizados se moverán automáticamente de \"%1$s\" a \"%2$s\" ya que su derivación pertenece a esa cuenta. + Algunos tokens personalizados se moverán automáticamente ¿No encuentra el token en su billetera? Consulte la sección de Mercados para encontrarlo y añadirlo a la compra ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta Vender @@ -79,6 +122,11 @@ Agregar billetera Seleccione una billetera para iniciar sesión ¡Bienvenido de nuevo! + + %d tarjeta + %d tarjetas + + Billetera Móvil 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 @@ -339,6 +387,7 @@ Transferencia No se pueden cargar los datos… Entiendo + Entiendo, continuar Hubo un error. Por favor inténtelo de nuevo. Inaccesible Termine el staking @@ -350,6 +399,9 @@ Modo de rendimiento Dirección de contrato copiada Redes disponibles + La derivación de su token coincide con la derivación de %1$s. Su token se añadirá a esta cuenta. + La derivación pertenece a otra cuenta. + Token añadido a la cuenta %1$s Dirección de contrato La dirección de contrato es incorrecta Por favor, seleccione la red @@ -411,6 +463,7 @@ Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. + Intercambie cualquier activo de su cartera por este token Mis tokens Aún no ha añadido ningún token. Agregue tokens a través de Mercados para hacer intercambios No se puede intercambiar por %s @@ -486,6 +539,12 @@ ID : %s ID de transacción copiado Convierta una de sus activos de su portafolio por este token + Una mayor velocidad significa una confirmación más rápida y una tarifa de red más alta. %s + Elija la velocidad + Elija qué token utilizar para pagar la tarifa de red. %s + Elija token + Mercado y noticias + De actualidad La siguiente información es opcional. Puede borrarla si no quiere compartirla. Cuéntenos qué funciones echa de menos y trataremos de ayudarle. Por favor, díganos qué tarjeta o anillo tiene @@ -638,6 +697,7 @@ El token seleccionado no está disponible actualmente para acciones dentro de la billetera cripto. Pero no se preocupa, puede expresar su interés votando a favor. Votar a favor La billetera no admite más de una red + Sobre la moneda Para comprar, intercambiar o recibir este activo, agréguelo a su portafolio Este activo no está disponible en la billetera Este activo no está disponible para esta billetera @@ -673,8 +733,10 @@ Top Ganadores Top Perdedores Tendencias + Modo Rendimiento Staking es la forma más fácil de recibir recompensas por sus criptomonedas. %s Gana hasta %s APY + Token añadido Acerca de %s %d intercambio @@ -745,6 +807,21 @@ Activar Modo Rendimiento Debes actualizar a %1$s para crear una mobile wallet Mobile Wallet requiere %1$s o posterior + Todas las noticias + Me Gusta + + Hace %dh + Hace %dh + + + Hace %d minuto + Hace %d minutos + + Resumen rápido + Noticias relacionadas + Tokens relacionados + Fuentes + Manténgase informado NFC no está disponible en su dispositivo Acerca de NFT Activo NFT @@ -850,12 +927,14 @@ Generar claves de forma privada Su tarjeta está activada y lista para usar ¡Éxito! + ¡Su billetera está configurada y lista para usar! En este caso, necesitará empezar desde el principio. ¿Quiere salir del proceso de activación? Inicializando Ya se ha creado otra billetera en la tarjeta que está intentando agregar. Si tiene fondos en esta billetera, por favor retírelos y luego reinicie esta tarjeta y agréguela como backup. Guardar su billetera Creando un backup + Biometría Leer más sobre seed phrase @@ -909,6 +988,7 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Todas las ofertas Disponible con %s La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos. Buscar por país @@ -916,13 +996,26 @@ Otras monedas Monedas fiduciarias populares Buscar por moneda + Esta transacción ya ha sido procesada. No se requiere ninguna otra acción. + Obteniendo las mejores tarifas... + Instantáneo Al utilizar la funcionalidad onramp, acepta %1$s y %2$s del proveedor. + El servicio es proporcionado por un proveedor externo. \nTangem no es responsable. El monto de la compra no debe ser mayor a %s La cantidad a comprar debe ser como mínimo %s No hay proveedores disponibles para esta moneda + Más rápido Pagar con + Método de pago Disponible hasta %s Disponible desde %s + + %d proveedor + %d proveedores + + Proveedores + Utilizado recientemente + Recomendado Podrá completar su transacción en el proveedor externo, %s Redirigiendo a %s... Nuestros servicios no están disponibles en este país @@ -931,8 +1024,19 @@ Seleccione el país correcto para garantizar opciones de pago y servicios precisos. Ajustes Puede comprobar el estado de la transacción desde la página del token. + + hasta %d día + hasta %d días + + %s mín. + Disponible desde + Disponible hasta + Obtiene + El servicio es proporcionado por un proveedor externo. \nTangem no es responsable. Puede comprobar el estado de la transacción desde la página detallada del token + Hasta Vía + Pagará Agrupar Por saldo Organizar tokens @@ -956,6 +1060,7 @@ Envíe solo %s a esta dirección. Enviar cualquier otra moneda resultará en su pérdida irreversible. Envíe solo %1$s en la red %2$s Transfiera fondos desde otra billetera o exchange + Dirección para recompensas Participar Error al cargar la información sobre el programa de referidos. Por favor, inténtelo de nuevo más tarde. Error al cargar la información sobre el programa de referidos. Código de error: %s. Por favor, inténtelo de nuevo más tarde. @@ -988,8 +1093,13 @@ Resetear la tarjeta Entiendo que después de realizar esta acción, ya no tendré acceso a la billetera actual Entiendo que no puedo usar esta tarjeta para recuperar mi código de acceso en las otras tarjetas de la wallet actual + Entiendo que perderé completamente el acceso a mi tarjeta Tangem Pay y a todos los fondos que contenga sin posibilidad de recuperación El restablecimiento de fábrica eliminará completamente la billetera de la tarjeta seleccionada. No podrá restaurar la billetera actual ni usar la tarjeta para recuperar el código de acceso. 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. + Se han restablecido todos los dispositivos Tangem. + Hubo un problema con la activación. Por favor, restablezca las tarjetas una a una. + Error en la verificación de la tarjeta + Por favor, reinicie el siguiente dispositivo para continuar 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. @@ -1068,6 +1178,7 @@ Los aranceles aduaneros son altos Debido a las peculiaridades de la red %1$s, la tarifa por transferir el saldo completo es más alta. Para reducir la comisión, puedes dejar %2$s. La tarifa es más alta + La cuenta del beneficiario no está activada. El importe mínimo de la transferencia debe ser igual o superior al saldo exento de alquiler: %1$s. Su saldo no puede ser inferior al monto del alquiler. Mantenga al menos %1$s en su cuenta o retire todos los fondos. La comisión incluida excede el monto de la transferencia, lo que resulta en un valor negativo Cantidad no válida @@ -1133,15 +1244,21 @@ Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre Haga que su token trabaje para Ud. + Una tarifa de red es un pequeño pago necesario para procesar y confirmar su transacción en la blockchain. Para comenzar a hacer staking, debe activar su cuenta TON con una auto-transacción de 1 TON. Los fondos permanecerán en su billetera; este paso solo es necesario para habilitar su cuenta para staking. + Activación de cuenta + La tarifa de red ha cambiado. Por favor, revise el nuevo importe antes de continuar. + Tarifa de red actualizada La cantidad del staking debe ser al menos %s La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red. La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red. + APR %1$s%% Reclamar lo que no está en staking Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. Porcentaje de rendimiento anual APR: Muestra el interés que podrías ganar en un año sin capitalización. Los intereses ganados no se añaden a tu saldo, por lo que no ganas intereses adicionales sobre ellos. + Porcentaje de rendimiento anual APY: Muestra el total de intereses que podrías ganar en un año con capitalización. La capitalización significa que los intereses ganados se añaden a tu saldo, por lo que también ganas intereses sobre esos intereses. APR APY @@ -1170,6 +1287,7 @@ La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking. Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor Bloqueado + Importe máximo: %s Migrar Staking nativo No hay validadores activos disponibles para hacer staking en este momento. Por favor, inténtalo de nuevo más tarde. @@ -1200,6 +1318,7 @@ La opción de volver a staking le permite mover sus fondos de un validador a otro sin necesidad de retirarlos. Está a punto de hacer staking con todo su saldo. Recomendamos dejar una pequeña cantidad para cubrir las tarifas de red para hacer unstaking o reclamar recompensas. Para empezar a hacer staking, primero debes activar tu cuenta de TON. + Activación de cuenta Para empezar a hacer staking en TON, realice primero una transacción de salida de cualquier importe - esto activará su billetera. Es posible que se requieran hasta 0.2 TON además de la tarifa de red para completar la transacción. Cualquier cantidad no utilizada será reembolsada. Se requieren 0.2 TON para realizar esta operación, además de la tarifa de red. Por favor, recargue su saldo. @@ -1298,6 +1417,7 @@ Error en la estimación de la tarifa. Envíe sus comentarios al servicio de asistencia. Usted intercambia Hacer un intercambio de esta cantidad del token seleccionado causará un impacto significativo en el precio y reducirá su resultado. + Alto impacto en los precios Fondos insuficientes Dar autorización Intercambiar @@ -1320,6 +1440,7 @@ Tu tarjeta está congelada. Obtener ayuda Otro + No se puede usar en dispositivos rooteados Completado Rechazado Pendiente @@ -1332,6 +1453,8 @@ No se pudo descongelar la tarjeta. Inténtalo de nuevo más tarde. Tu tarjeta está descongelada. Retirada + No se puede usar en un dispositivo rooteado + Cancelar KYC Agregar fondos Opciones de recarga Número de tarjeta @@ -1359,19 +1482,24 @@ ¡Todo listo! Tu tarjeta está lista para usar. Añadir tarjeta a Google Pay Añade tu tarjeta a Apple Pay + Código PIN Comparte tu dirección o muestra el código QR + Se detectaron problemas técnicos. Inténtelo de nuevo más tarde o póngase en contacto con el servicio de asistencia. Recepción no disponible ahora Mostrar Mostrar detalles Intercambia cualquier activo de tu portafolio por una tarjeta Detalles de la tarjeta Descongelar tarjeta + Vuelva a la aplicación si lo olvida. + Su código PIN Retirar Retiro no disponible ahora No puedes iniciar un intercambio o un nuevo retiro hasta que el actual finalice. Retiro en progreso Cambiar código PIN Vuelve a la app si lo olvidas. + Entiendo que perderé completamente el acceso a mi tarjeta Tangem Pay y a todos los fondos que contenga sin posibilidad de recuperación 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 @@ -1381,13 +1509,16 @@ Suele tardar hasta 15 minutos Configurando tu tarjeta Tangem Emisión de su tarjeta - Estamos preparando tu tarjeta. Esto puede llevar un poco de tiempo. + La tarjeta suele emitirse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, la emisión puede tardar hasta 48 horas. Tangem Pay + Confirmar cancelación + ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad KYC en curso Ver estado KYC en progreso para Tangem Pay + Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1404,7 +1535,9 @@ 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. + Establecer \nCódigo PIN Sincronización necesaria + Restablecer acceso Usa USDC para pagos cotidianos Tangem Pay temporalmente no disponible Tangem Pay @@ -1438,6 +1571,7 @@ Token en la red %%image%% %1$s El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista No se puede ocultar %s + Mostrar código QR Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambie con Changelly, %s comisiones Intercambie ahora @@ -1452,6 +1586,7 @@ Múltiples direcciones El historial de transacciones no está soportado actualmente para esta blockchain. Pero no se preocupe, ¡estamos trabajando en ello! Mientras tanto, puede consultarlo en el explorador. Operación + para: %s desde: %s a: %s validador: %s @@ -1498,11 +1633,81 @@ Renombrar la billetera Desbloquear todo Desbloquear todo con %s + AML verificado + Disponible + Bloqueado + Deuda + Límites + Otros (no-otp) + Transacción única + Total + + disponible por %d día + disponible por %d días + + El saldo disponible son los fondos reales disponibles, teniendo en cuenta las operaciones pendientes, los importes bloqueados y el saldo deudor para evitar descubiertos. + Disponible hasta %1$s + Saldos y límites + Falta información de la cuenta de pago. Contacte con el soporte técnico. + No se puede cargar información sobre el token + La información aún se está cargando, por favor espere. + Los límites son necesarios para controlar los costes, mejorar la seguridad y gestionar el riesgo. Puede gastar %1$s durante el periodo para pagos con tarjeta en comercios y %2$s para otras transacciones, por ejemplo, suscripciones o deudas. + El código de acceso se utilizará para gestionar su cuenta de pago y protegerla de accesos no autorizados + Código de acceso + Activación de cuenta + Por favor, seleccione la billetera con la que inició el proceso de registro para firmar la transacción de creación de su cuenta en Blockchain. + Cancelar Activación + ¿Seguro que quiere salir? Puede continuar más tarde desde donde lo dejó. + Esto no tardará mucho. Estamos configurando tu cuenta. + Esto no tardará mucho. Estamos completando la activación. + ¡Preparando todo! + Otra billetera Configure un código de 4 dígitos.\nSe utilizará para los pagos. + Código PIN Crear código PIN No se aceptó el PIN. Inténtalo de nuevo o usa un código diferente. PIN no válido: evitar secuencias o repeticiones + ¡Está listo para empezar! + Prepare la tarjeta Tangem y toque para aprobar + Prepare su Billetera Tangem + Podrá completar su conexión \n en un sitio web de terceros \n y volver a la aplicación Tangem + Ir al sitio web + Conexión de billetera + Elija su billetera + Continuar Activación + Continuemos configurando su cuenta. + ¡Bienvenido de nuevo! + Iniciar Activación + Siga los pasos para configurar su cuenta. + ¡Bienvenido! + Cantidad en blockchain + Código de moneda + Fecha + Código de error + Detalles de la transacción + Código de categoría de comerciante + Ciudad del comercio + Código de país del comercio + Nombre del comercio + ID Solicitud + Estado + Transacción + Importe de la transacción + Hash de la transacción + Solicitud de transacción + Estado de la transacción + Tipo + Impugnar esta transacción + Desbloquear + Escanee su tarjeta para desbloquear el acceso + Desbloqueo necesario Elija cómo agregar su billetera + Escanee su tarjeta o anillo Tangem para restaurarlo o importarlo desde otra billetera. + Crear una billetera de hardware + ¿Quiere comprar una  Billetera Tangem? + Importar seed phrase + Restaure su billetera en su teléfono o impórtela desde otra aplicación: cómodo, pero menos seguro que una tarjeta Tangem. + ¿Qué elegir? La blockchain está Inaccesible. Inténtelo más tarde Escanee la tarjeta o el anillo Esta billetera ya ha sido activada anteriormente.\nSi no fue usted quien lo hizo, comuníquese con el servicio de asistencia.\nTangem nunca vende billeteras con un código de acceso pre generado. @@ -1523,6 +1728,8 @@ Tarjeta/anillo incorrecta (o) seleccionada en Tangem App Error al crear la transacción a partir de los datos de la dApp. Código: %s Hemos encontrado un error desconocido. Mensaje de error: %d. Si el problema persiste, contacte con nuestro soporte + Transacción en varias partes + Para que se procese correctamente, su transacción se dividirá en varias partes. Tendrá que tocar su tarjeta varias veces para completarla. No hay sesiones abiertas de WalletConnect ¡Qué lástima! No hay sesiones. Error al emparejar la sesión de WalletConnect: %1$s @@ -1534,11 +1741,22 @@ Esta tarjeta no se puede usar para establecer una sesión de WalletConnect Esta red no es compatible. Por favor, seleccione otra red. Seleccione una red + La transacción se está procesando. Toque su tarjeta varias veces para completarla. + Transacción en curso Sesiones de WalletConnect Conectar a dApps WalletConnect La conexión puede tardar unos segundos. Crear una billetera Tangem + Desde %s + Compre Tangem Wallet, un dispositivo físico que almacena de forma segura su clave privada sin conexión a internet. + Billetera de hardware + Crea una billetera segura en su teléfono en segundos. + Billetera Móvil + ¿Qué elegir? + ¿Ya está usando una Billetera Tangem? + Escanear ahora + Elija un método de configuración de billetera ¿Quiere comprar Tangem Wallet? Comprar ahora Recuperar una billetera existente mediante copia de seguridad de Google Drive @@ -1549,7 +1767,7 @@ Importa una billetera existente con su frase de recuperación. Importar billetera Introduzca la frase de recuperación - La copia de seguridad de su billetera se realizó con éxito. + Su billetera se ha importado con éxito. Importar billetera Importación completada Importar billetera @@ -1605,6 +1823,7 @@ Esto puede suceder porque el proveedor actualmente no puede operar con el par que usted seleccionó. Espere un momento e inténtelo de nuevo. (Código %s) El par seleccionado no está disponible temporalmente Algunos proveedores no están autorizados por la Autoridad de Conducta Financiera del Reino Unido. Debe evitar tratar con ellos. + Lista de advertencias de la FCA Servicio no disponible temporalmente La cantidad de tokens a intercambiar no debe exceder %s El monto a cambiar debe ser de al menos %s diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 16ebbde2fe..a1a06768bb 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -16,6 +16,52 @@ Définissez un code d\'accès à %s chiffres pour déverrouiller votre portefeuille. Créer un code d\'accès Code d\'accès + Vous ne pouvez pas créer plus de %1$s comptes. Archivez-en un pour en ajouter un nouveau. + Impossible d\'ajouter un nouveau compte + Compte archivé + Comptes archivés + Récupérer + Vous êtes sur le point de récupérer « %1$s ». + Récupérer le compte + Vous avez atteint la limite de 20 comptes actifs. Archivez un compte pour récupérer celui-ci. + Impossible de récupérer le compte + Archivé + Nous n\'avons pas pu archiver le compte. Veuillez réessayer plus tard. + Ce compte participe au programme de parrainage. + Ce compte ne peut pas être archivé. + Nous n\'avons pas pu créer de compte. Veuillez réessayer plus tard. + Compte créé + Compte archivé + Archives + Vous archivez ce compte, mais vous pouvez toujours le récupérer. + Archivage... + Compte + Impossible de modifier le compte + Compte enregistré + Compte pour les récompenses + Compte n° %s — utilisé pour la dérivation d\'adresse. + Ajouter un compte + Enregistrer + Nom du compte + Nom de compte déjà utilisé + Compte + Nouveau compte + Ajouter un compte + Modifier le compte + Veuillez réessayer plus tard. Si le problème persiste, contactez le service d\'assistance et nous vous aiderons à le résoudre. + %1$s dans %2$s + Compte principal + Vous avez déjà dépassé la limite de %1$s comptes actifs. Archivez-en un pour en récupérer un autre. + Impossible de récupérer le compte + Compte récupéré + Appuyez longuement sur un compte pour réorganiser les comptes. + Continuer à modifier + Supprimer + Êtes-vous sûr de vouloir supprimer le nouveau compte ? + Êtes-vous sûr de vouloir supprimer les modifications ? + Modifications non enregistrées + Certains jetons personnalisés seront automatiquement déplacés de « %1$s » vers « %2$s », car leur dérivation appartient à ce compte. + Certains jetons personnalisés seront automatiquement déplacés. Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'achat Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à la vente Vendre @@ -51,14 +97,17 @@ Avez-vous des difficultés à scanner votre carte ? Cette carte n\'est pas conçue pour fonctionner avec Tangem Définissez d\'abord un code d\'accès pour activer la biométrie. + Utilisez %1$s pour déverrouiller votre portefeuille et approuver les actions sensibles, telles que la signature de transactions. Pour les portefeuilles matériels, une carte ou une bague est toujours nécessaire pour signer. Frais par défaut Activez les frais par défaut pour définir automatiquement les frais de transaction et ignorer la page Frais lors de l\'envoi de fonds. Vous pouvez toujours revenir sur cette page si nécessaire. Accédez aux paramètres pour activer l\'authentification biométrique dans l\'application Tangem Activer l\'authentification biométrique + La désactivation de %1$s vous obligera à saisir votre code d\'accès pour déverrouiller l\'application et interagir avec votre portefeuille. Le code d\'accès à votre portefeuille vous sera demandé ultérieurement afin que nous puissions le stocker en toute sécurité pour une utilisation future. Cela supprimera tous les codes d\'accès des portefeuilles enregistrés. Toute opération ultérieure avec le portefeuille nécessitera la soumission du code d\'accès. La suppression de la carte enregistrée supprime de l\'application tous les portefeuilles enregistrés et leurs codes d\'accès. Cela supprimera tous les codes d\'accès enregistrés pour le portefeuille. Toute interaction ultérieure avec le portefeuille nécessitera la saisie du code d\'accès. + Code d\'accès requis Cette option désactive la biométrie pour les actions sensibles. Vous devrez saisir votre code d\'accès à chaque fois que vous signerez une transaction. Enregistrer le code d\'accès L\'authentification biométrique sera demandée à la place du code d\'accès pour les interactions avec votre carte. @@ -72,6 +121,11 @@ Ajouter un wallet Sélectionnez un wallet pour vous connecter Heureux de te revoir! + + %d carte + %d cartes + + Mobile Wallet 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 @@ -332,6 +386,7 @@ Fourniture Impossible de charger les données… Je comprends + Je comprends, continuer. Il y avait une erreur. Veuillez réessayer. Inaccessible Unstakez @@ -343,6 +398,7 @@ Mode Rendement Adresse du contrat copiée ! Réseaux disponibles + Jeton ajouté au compte %1$s Adresse du contrat L\'adresse du contrat n\'est pas valide Veuillez sélectionner le réseau @@ -404,6 +460,7 @@ Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille Pas assez de fonds pour la transaction. Veuillez recharger votre compte. + Échangez n\'importe quel actif de votre portefeuille contre ce jeton. Mes jetons Vous n\'avez pas encore ajouté de jetons. Ajoutez des jetons via Market pour effectuer une échange Ne peut pas être échangé contre %s @@ -442,6 +499,7 @@ En attente du dépôt En attente du dépôt Remboursé + En attente de remboursement Transfert à votre compte En cours d\'envoi Envoyé @@ -459,6 +517,7 @@ Meilleur taux Liste d’avertissement de la FCA Meilleur choix + Fournisseur figurant sur la liste d\'avertissement de la FCA Disponible jusqu\'à %s Disponible à partir de %s Indisponible pour cette paire @@ -477,6 +536,12 @@ ID : %s ID de transaction copié Convertissez une de vos devises en une autre + Une vitesse plus élevée signifie une confirmation plus rapide et des frais de réseau plus élevés. %s + Choisissez la vitesse + Choisissez le token à utiliser pour payer les frais de réseau. %s + Choisissez un token + Marché et actualités + Tendances actuelles Les informations suivantes sont facultatives. Vous pouvez les effacer si vous ne souhaitez pas les partager. Dites-nous quelles fonctions vous manquent, et nous essaierons de vous aider. Veuillez nous dire quelle carte vous avez @@ -629,6 +694,7 @@ Le jeton sélectionné n\'est actuellement pas disponible pour des actions dans le portefeuille crypto. Mais ne vous inquiétez pas, vous pouvez exprimer votre intérêt en votant positivement. Vote positif Le portefeuille ne prend pas en charge plus d\'un réseau + À propos de la crypto Pour acheter, échanger ou recevoir cet actif, ajoutez-le à votre portefeuille Cet actif n\'est pas pris en charge dans le portefeuille Cet actif n\'est pas disponible pour ce portefeuille @@ -664,8 +730,10 @@ Meilleurs Gagnants Top Perdants Tendances + Mode rendement Le staking est le moyen le plus simple de recevoir des récompenses sur votre crypto. %s Gagnez jusqu\'à %s APY + Jeton ajouté À propos de %s %d échange @@ -736,6 +804,21 @@ Activer le mode rendement Vous devez effectuer la mise à jour %1$s afin de créer un portefeuille mobile. Le portefeuille mobile nécessite %1$s ou une version supérieure. + Toutes les actualités + Like + + Il y a %dh + Il y a %dh + + + Il y a %d minute + Il y a %d minutes + + Récapitulatif rapide + Actualités similaires + Tokens associés + Sources + Restez informé NFC n\'est pas disponible sur votre appareil À propos des NFT Actif NFT @@ -841,20 +924,22 @@ Générer des clés de manière privée Votre carte est activée et prête à être utilisée Succès ! + Votre portefeuille est configuré et prêt à être utilisé ! Dans ce cas, vous devrez recommencer depuis le début. Voulez-vous quitter le processus d\'activation ? Initialisation en cours Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. Enregistrer votre portefeuille Sauvegarde en cours + Biométrie En savoir plus sur les seed phrases - + Vide Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase - + Vide %d mots Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous @@ -900,6 +985,7 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Toutes les offres Disponible avec %s L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions. Recherche par pays @@ -907,13 +993,26 @@ Autres devises Devises Fiats Populaires Recherche par devise + Cette transaction a déjà été traitée. Aucune autre action n\'est requise. + Recherche des meilleurs tarifs... + Instantané En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur + Le service est fourni par un prestataire externe. Tangem n\'est pas responsable. Le montant de l\'achat ne doit pas dépasser %s Le montant à acheter doit être au moins %s Aucun fournisseur disponible pour cette devise + Le plus rapide Payer avec + Mode de paiement Disponible jusqu\'à %s Disponible à partir de %s + + %d fournisseur + %d fournisseurs + + Fournisseurs + Récemment utilisé + Recommandé Vous pourrez finaliser votre transaction sur le fournisseur tiers, %s Redirection vers %s... Nos services ne sont pas disponibles dans ce pays @@ -922,8 +1021,18 @@ Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis. Paramètres Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton. + + jusqu\'à %d jour + jusqu\'à %d jours + + %s min + Disponible auprès de + Disponible jusqu\'à + Vous obtenez Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton + Jusqu\'à Via + Vous paierez Grouper Par solde Organiser les jetons @@ -947,6 +1056,7 @@ Envoyez uniquement %s à cette adresse. L\'envoi de toute autre devise entraînera sa perte irréversible. Envoyez uniquement %1$s sur le réseau %2$s Transférez des fonds depuis un autre portefeuille ou une autre bourse + Adresse pour les récompenses Participer Échec du chargement des informations sur le programme de parrainage. Veuillez réessayer plus tard. Échec du chargement des informations sur le programme de parrainage. Code d\'erreur : %s. Veuillez réessayer plus tard. @@ -979,8 +1089,13 @@ Réinitialiser la carte Je comprends qu\'après avoir effectué cette action, je n\'aurai plus accès au portefeuille actuel Je comprends que je ne peux pas utiliser cette carte pour récupérer mon code d\'accès sur les autres cartes du portefeuille actuel + Je comprends que je perdrai complètement l\'accès à ma carte Tangem Pay et à tous les fonds qui s\'y trouvent, sans possibilité de récupération. La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée. Vous ne pourrez pas restaurer le portefeuille actuel ni utiliser la carte pour récupérer le code d\'accès. La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée et le supprimera de l\'application. Vous ne pourrez pas restaurer le portefeuille actuel. + Tous les appareils Tangem ont été réinitialisés. + Une erreur s\'est produite lors du processus d\'activation. Veuillez réinitialiser les cartes une par une. + Échec de la vérification de la carte + Veuillez réinitialiser l\'appareil suivant pour continuer. Les propriétaires du Tangem Ring ont droit à 3 swap gratis sur Changelly jusqu\'au 15/11 ! Échangez avec 0 % de frais ! Les appareils disposant d\'un accès root sont considérés comme moins sécurisés. Vos données peuvent être exposées à des risques supplémentaires. @@ -1126,14 +1241,18 @@ Nom Faites travailler votre jeton pour vous Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking. + Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer. + Frais de réseau mis à jour Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. + TAEG %1$s%% Réclamation déstakée Frais de staking du compte Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé. Pourcentage de rendement annuel APR : Montre le taux d’intérêt que vous pourriez gagner en un an sans capitalisation. Les intérêts gagnés ne sont pas ajoutés à votre solde, donc vous ne gagnez pas d’intérêts supplémentaires dessus. + Taux de rendement annuel APY : Montre le total des intérêts que vous pourriez gagner en un an avec capitalisation. La capitalisation signifie que les intérêts gagnés sont ajoutés à votre solde, et vous gagnez également des intérêts sur ces intérêts. APR APY @@ -1162,6 +1281,7 @@ Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement. En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué + Montant maximal : %s Migrer Native staking Aucun validateur actif n\'est disponible pour le staking pour le moment. Veuillez réessayer plus tard. @@ -1290,6 +1410,7 @@ Erreur d\'estimation des frais. Veuillez envoyer vos commentaires à l\'équide de support. Vous échangez Échanger ce montant de jetons sélectionnés aura un impact significatif sur les prix et réduira votre résultat. + Impact élevé sur les prix Fonds insuffisants Donner l\'autorisation Échanger @@ -1312,6 +1433,7 @@ Votre carte est gelée. Obtenir de l\'aide Autre + Impossible à utiliser sur les appareils rootés Terminé Refusé En attente @@ -1324,6 +1446,8 @@ Échec du dégel de la carte. Réessayez plus tard. Votre carte est dégelée. Retrait + Impossible à utiliser sur un appareil rooté + Annuler KYC Ajouter des fonds Options de recharge Numéro de carte @@ -1351,19 +1475,24 @@ Tout est prêt ! Votre carte est prête à l\'emploi. Ajouter une carte à Google Pay Ajouter la carte à Apple Pay + code PIN Partagez votre adresse ou montrez le QR code + Problèmes techniques détectés. Veuillez réessayer plus tard ou contacter le service d\'assistance. Réception indisponible pour le moment Révéler Afficher les détails Échangez n\'importe quel actif de votre portefeuille contre une carte Détails de la carte Dégeler la carte + Revenez à l\'application si vous l\'oubliez. + Votre code PIN Retirer Retrait indisponible pour le moment Vous ne pouvez pas lancer d\'échange ou de nouveau retrait tant que le retrait actuel n\'est pas terminé. Retrait en cours Modifier le code PIN Revenez dans l\'application si vous l\'oubliez. + Je comprends que je perdrai complètement l\'accès à ma carte Tangem Pay et à tous les fonds qui s\'y trouvent, sans possibilité de récupération. É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 @@ -1373,13 +1502,16 @@ Cela prend généralement jusqu\'à 15 minutes Configuration de votre carte Tangem Émission de votre carte - Nous préparons votre carte. Cela peut prendre un peu de temps. + La carte est généralement émise automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, l’émission peut prendre jusqu’à 48 heures. Tangem Pay + Confirmer l\'annulation + Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment. Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support. Malheureusement, nous n\'avons pas pu vérifier votre identité KYC en cours Voir le statut KYC en cours pour Tangem Pay + Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte @@ -1396,7 +1528,9 @@ 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. + Définir le \ncode PIN Synchronisation requise + Restaurer l\'accès Utilisez USDC pour les paiements quotidiens Tangem Pay est temporairement indisponible Tangem Pay @@ -1408,6 +1542,7 @@ Annuler Cette opération est actuellement indisponible. Veuillez réessayer plus tard. L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter davantage d\'options. + L\'action n\'est pas disponible pour les jetons personnalisés. Vous n\'avez pas de fonds à vendre. Renflouez votre compte pour pouvoir vendre des fonds à partir de celui-ci. Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci. Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuchen Sie es später noch einmal. @@ -1443,6 +1578,7 @@ Adresses multiples L\'historique des transactions n\'est actuellement pas pris en charge pour cette blockchain. Mais ne vous inquiétez pas, nous y travaillons ! En attendant, vous pouvez le vérifier dans l\'explorateur. Opération + pour : %s de : %s à : %s validateur : %s @@ -1489,26 +1625,71 @@ Renommer le portefeuille Tout déverrouiller Tout déverrouiller avec %s + AML vérifié + Disponible + Bloqué + Dette + Limites + Autre (sans mot de passe) + Transaction unique + Total disponible pour %d jour disponible pour %d jours + Le solde disponible correspond aux fonds réellement disponibles, en tenant compte des transactions en attente, des montants bloqués et du solde débiteur afin d\'éviter les découverts. + Disponible jusqu\'au %1$s Soldes et Limites + Informations de paiement manquantes. Veuillez contacter le service d\'assistance. + Impossible de charger les informations relatives au jeton + Les informations sont en cours de chargement, veuillez patienter. + Des limites sont nécessaires pour contrôler les coûts, améliorer la sécurité et gérer les risques. Vous pouvez dépenser %1$s pendant la période pour les paiements par carte dans les magasins et %2$s pour les autres transactions, par exemple les abonnements ou les dettes. Le code d\'accès sera utilisé pour gérer votre compte de paiement et le protéger contre tout accès non autorisé. Code d\'accès + Activation du compte + Veuillez choisir le portefeuille avec lequel vous avez commencé le processus d\'inscription afin de signer la transaction pour créer votre compte sur la blockchain. + Annuler l\'activation Êtes-vous sûr de vouloir quitter ? Vous pourrez reprendre plus tard là où vous vous étiez arrêté. Cela ne prendra pas longtemps. Nous configurons votre compte. Cela ne prendra pas longtemps. Nous terminons l\'activation. Tout est en cours de préparation ! + Autre portefeuille Créez un code à 4 chiffres. Il servira pour les paiements. + code PIN Créer un code PIN Le code PIN n\'a pas été accepté. Veuillez réessayer ou utiliser un autre code. Code PIN invalide : évitez les séquences ou les répétitions + Vous êtes prêt à partir ! + Préparez la carte Tangem et appuyez pour approuver. + Préparez votre Tangem Wallet + Vous pourrez terminer votre connexion \n sur le site Web tiers \n et revenir à l\'application Tangem. Accéder le site Web + Connexion au portefeuille + Choisissez un portefeuille + Continuer l\'activation Continuons la configuration de votre compte. Content de vous revoir ! + Lancer l\'activation Suivez les étapes pour configurer votre compte. Bienvenue ! + Montant de la blockchain + Code monétaire + Date + Code d\'erreur + Détails de la transaction + Code de catégorie de commerçant + Cité marchande + Code pays du commerçant + Nom du commerçant + Identifiant de la demande + Statut + Transaction + Montant de la transaction + Hash de la transaction + Demande de transaction + Statut de la transaction + Type + Contester cette transaction Déverrouiller Scannez votre carte pour déverrouiller l\'accès Déverrouillage nécessaire @@ -1533,6 +1714,8 @@ Mauvaise carte/bague sélectionnée dans le Tangem App Échec de la création de la transaction à partir des données de la Dapp. Code: %s Nous avons rencontré une erreur inconnue. Code d\'erreur : %d. Si le problème persiste, n\'hésitez pas à contacter notre support + Transaction en plusieurs parties + Pour que le traitement soit effectué correctement, votre transaction sera divisée en plusieurs parties. Vous devrez passer votre carte plusieurs fois pour la finaliser. Aucune session WalletConnect ouverte Oups. Aucune session. Échec de l\'appairage de la session WalletConnect : %1$s @@ -1544,11 +1727,22 @@ Cette carte ne peut pas être utilisée pour établir une session WalletConnect Ce réseau n\'est pas pris en charge. Veuillez sélectionner un autre réseau. Sélectionnez un réseau + La transaction est en cours de traitement. Veuillez taper plusieurs fois votre carte pour la terminer. + Transaction en cours Sessions WalletConnect Se connecter aux dApps WalletConnect La connexion peut prendre quelques secondes Créer un Tangem Wallet + À partir de %s + Achetez Tangem Wallet, un dispositif physique qui stocke votre clé privée hors ligne en toute sécurité. + Hardware Wallet + Créez un portefeuille sécurisé sur votre téléphone en quelques secondes. + Mobile Wallet + Quoi choisir ? + Vous utilisez déjà un Tangem Wallet ? + Scanner maintenant + Choisissez une méthode de configuration du portefeuille Vous souhaitez acheter le Tangem Wallet ? Acheter maintenant Récupérer un portefeuille existant via la sauvegarde Google Drive @@ -1559,7 +1753,7 @@ Importez un portefeuille existant à l\'aide de votre seedphrase. Importer un wallet Entrez la seedphrase - Vous avez sauvegardé votre portefeuille avec succès. + Vous avez importé votre portefeuille avec succès. Importer un wallet Importation terminée Importer un portefeuille @@ -1727,6 +1921,7 @@ Connexions Contenu Copier les données + Allocation personnalisée dApp déconnectée Déconnecter tout Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. @@ -1739,6 +1934,8 @@ La proposition de connexion a expiré Modifications estimées du portefeuille La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence. + L\'estimation n\'est pas prise en charge pour %s. + Suggéré par %s Rechargez votre solde pour couvrir les frais de réseau. %1$s Insuffisant Transaction malveillante @@ -1752,6 +1949,7 @@ Risque de sécurité connu Ouvrez l\'application Web3 et sélectionnez l\'option WalletConnect. Demande de + Signer quand même Type de signature Au moins un réseau est requis pour la connexion à une dApp. Spécifier les réseaux sélectionnés @@ -1760,7 +1958,10 @@ Demande de transaction Demande de transaction Montant illimité + Assurez-vous que chaque tentative d\'appairage utilise un URI unique et nouveau. + URI déjà utilisé WalletConnect + Transaction suspecte Vous avez déjà un portefeuille Tangem ? Des milliers d\'actifs Le meilleur hardware wallet de sa catégorie diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 9128efeb7f..cfa982d89a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -145,13 +145,14 @@ Di solito richiede fino a 15 minuti Configurazione della tua carta Tangem Emissione della tua carta - Stiamo preparando la tua carta. Ci vorrà un po\' di tempo. + La carta viene solitamente emessa automaticamente entro 5 minuti. In rari casi, se è necessaria una revisione manuale, l’emissione può richiedere fino a 48 ore. Tangem Pay Non siamo riusciti a verificare il tuo profilo. Per domande, contatta il supporto. Purtroppo non siamo riusciti a verificare la tua identità KYC in corso Visualizza stato KYC in corso per Tangem Pay + I documenti vengono solitamente verificati automaticamente entro 5 minuti. In rari casi, se è necessaria una revisione manuale, il processo può richiedere fino a 48 ore. Ottieni la tua carta virtuale Tangem Visa gratuita Usa USDC per i pagamenti quotidiani Ottieni carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 666fe8e6bb..785a92204c 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -61,8 +61,8 @@ 新しいアカウントを破棄してもよろしいですか? 編集内容を破棄してもよろしいですか? 保存されていない変更 - 一部のカスタムトークンは、その派生がそのアカウントに属しているため、「 %1$s 」から「 %2$s 」に移動されました。 - 一部のカスタムトークンが移動されました + 一部のカスタムトークンは、その派生元(デリベーション)が該当アカウントに属しているため、自動的に「%1$s」から「%2$s」へ移動されます。 + 一部のカスタムトークンは自動的に移動されます トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -127,15 +127,15 @@ モバイルウォレット ウォレットのバックアップが正常に完了しました。 - これらの単語は一度失うと復元できません。安全な場所に保管してください。 + これらの単語は、紛失すると復元できません。安全に保管してください。 バックアップが完了しました - シークレットリカバリーフレーズとは、ウォレットへのアクセスや復元のために使う、固定された%s個のランダムな単語のセットです。 + シークレットリカバリーフレーズは、ウォレットへのアクセスおよび復元に使用する、%s個のランダムな単語のセットです。 これらの単語は紛失した場合、復元できません。大切に保管してください。 安全に保管してください これらの%s個の単語を安全な場所に保存し、決して誰とも共有しないでください。 復元は不可能です リカバリーフレーズ - これらの単語は誰にも教えないでください。Tangemがこれらの単語を尋ねることは、絶対にありません。以下の%s個の単語はウォレットのリカバリーフレーズです。デバイスを紛失した場合、これらの単語を使用してウォレットを復元できます。 + 以下の%s個の単語が、あなたのウォレットのリカバリーフレーズです。これらの単語は絶対に他人と共有しないでください。Tangemがこれらを尋ねることは決してありません。デバイスを紛失した場合、このフレーズを使ってウォレットを復元できます。 これらの%s個の単語を番号順に書き留め、安全かつ他人の目に触れない場所に保管してください ウォレットの安全確保およびリカバリーフレーズの安全なバックアップは、すべてご自身の責任となります。 リカバリーフレーズ @@ -317,6 +317,7 @@ いいえ アドレスがありません 未追加 + 利用できません 今はしない わかりました @@ -531,6 +532,10 @@ ID: %s 取引IDをコピーしました ポートフォリオ内のアセットをこのトークンと交換する + 速度を上げるほど承認は速くなりますが、\nネットワーク手数料も高くなります。%s + 速度を選択 + \nネットワーク手数料の支払いに使用するトークンを選択します。%s + トークンを選択 マーケット&ニュース Tangem AI いま話題 @@ -539,6 +544,7 @@ お持ちのカードまたはリングについて教えてください サポートチームの皆さん、こんにちは。 問題について詳しく教えてください。どんな些細なことでも役に立ちます。 + バックアップの問題 以前に有効化されたウォレット 私の提案 カード / リングをスキャンできません @@ -546,6 +552,7 @@ Tangemへのフィードバック 取引を送信できません コインの説明エラー + ガス代不要の取引手数料 エラーが発生しました エラーが発生しました。コード: %s 。 メモが必要 @@ -557,7 +564,7 @@ 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください 許可を与える 無制限 - バックアップは2枚または3枚のTangemカードを使って作成されます。紛失や破損に備えて、それぞれ別々の安全な場所に保管してください—シードフレーズは不要です。 + バックアップは2〜3枚のTangemカードを使用して作成されます。紛失や破損に備え、カードはそれぞれ安全な別の場所に保管してください。シードフレーズは不要です。 複数のカードでバックアップ Tangemウォレットを追加 あなたの秘密鍵はTangemカード内部で直接生成され、決してカードの外に出ることはありません。 @@ -578,7 +585,7 @@ ウォレットの設定を完了する アクセスコードでアプリを保護して、設定を完了してください。 終了すると、最初からやり直す必要があります。 - セットアップを中止してもよろしいですか? + アクティベーションを終了してもよろしいですか? 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する @@ -587,7 +594,7 @@ 新しいウォレットを作成 Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。 ハードウェアウォレット - 現在のウォレットをTangemに移します。 + 現在のモバイルウォレットをTangemのコールドウォレットに変換します。 現在のウォレットをアップグレードする バックアップへ移動 アクセスコードを作成する前にウォレットをバックアップしてください。 @@ -612,7 +619,7 @@ 本当にこれを実行しますか? ウォレット削除 バックアップへ移動 - バックアップを表示 + リカバリーフレーズを表示 ウォレット削除 とにかく削除する このウォレットにはバックアップがあります。ウォレットを削除する前に、復元できることを確認してください。 @@ -628,7 +635,7 @@ 操作中にエラーが発生しました。 処理中も、資金は安全に保たれ、常にアクセス可能です。 資金へのアクセス - ウォレットのデータはアプリから削除され、ハードウェアウォレットに保存されます + ウォレット情報はアプリから削除され、ハードウェアウォレットに保存されます セキュリティ全般 秘密鍵はアプリからTangemハードウェアウォレットへ移行されます キーの移行 @@ -802,7 +809,8 @@ クイックまとめ 関連ニュース - 情報源 + 関連トークン + 関連ニュース 最新情報を入手 お使いのデバイスではNFCが使用できません NFTについて @@ -884,8 +892,8 @@ 操作を繰り返してください。カードは工場出荷時の設定にリセットされます。 アクティベーションに失敗しました トークンを追加する - バックアップカードまたはリングを1つ追加しました。バックアップ処理が終了すると、これ以上バックアップデバイスを追加することはできません。もう一つカードまたはリングがある場合、バックアップに追加してください。バックアップ処理を続けますか? - バックアップ手続きは部分的に完了しています。現在終了することはできません。 + バックアップ用のカードまたはリングを1つ追加しました。バックアップが完了すると、これ以上デバイスを追加することはできません。追加のカードまたはリングをお持ちの場合は、今のうちに追加してください。続行しますか? + バックアップは一部完了しており、現在は中断できません。 パスフレーズは、リカバリーフレーズに単語やフレーズを追加することで、追加の保護を提供する任意のセキュリティ機能です。これにより、新しいウォレットアドレスのセットが作成されます。 バックアップ用のカードまたはリングを追加 カードをスキャン @@ -933,7 +941,7 @@ レガシー シードフレーズを正しく書き込んだかどうかを確認するために、2番目、7番目、11番目の単語を入力してください。 では、確認してみましょう - バックアップ処理を開始するには、最大2つのバックアップカードまたはリングを追加します。 + バックアップを開始するには、最大2枚のバックアップカードまたはリングを追加してください。 カードまたはリングをもう1つ追加するか、バックアップ処理を完了します 番号%sのバックアップカードを準備してください プライマリカードまたはリングをスキャンして、バックアップ処理を開始します。 @@ -966,6 +974,7 @@ アクセスコードの復元 同一のカード アクセスコード + 同時に利用できるのは、モバイルウォレットまたはハードウェアウォレットのいずれか一方のみです。Tangemのコールドウォレットを使用するには、ウォレットをアップグレードするか、アプリから削除してください。 すべてのオファー %sで利用可能 プロバイダーが取引を促進します @@ -1068,8 +1077,8 @@ この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。 このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました - 工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。 - 工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。 + 初期化を行うと、選択したカードまたはリングからウォレットは完全に消去されます。現在のウォレットを復元することはできず、このカードまたはリングを使ってアクセスコードを復旧することもできません。 + 初期化を行うと、選択したカードまたはリングからウォレットが完全に消去され、アプリからも削除されます。現在のウォレットは復元できません。 すべてのTangemデバイスがリセットされました。 アクティベーション処理中に問題が発生しました。カードを1枚ずつリセットしてください。 カード認証に失敗しました @@ -1232,10 +1241,12 @@ ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 年率 APR:1年間で得られる利息を単利で表示します。得た利息は残高に加算されず、その利息にはさらに利息がつきません。 + 年利(APY) APY:1年間で得られる合計利息を複利込みで表示します。複利とは、得た利息が残高に加算され、その利息にもさらに利息がつくことを意味します。 APR APY 報酬は毎日自動的にステーキング残高に蓄積されます。 + 報酬はステーキング残高に自動的に複利で加算されます。獲得した資金:%s 利用可能 平均報酬率 ステーキングの仕組み @@ -1353,7 +1364,7 @@ あなたのステーキング カードまたはリング内に秘密鍵を保管しながら、暗号資産を安全に保管します 革新的なハードウェアウォレット - 1つのウォレットに最大3枚のカードまたは3本のリング + 1つのウォレットに、最大3枚のカードまたはリングを追加 安全なバックアップ ビットコイン、イーサリアム、その他多くの暗号資産を同時に管理できるハードウェアウォレット – オール・イン・ワンのカード / リング 数千の通貨 @@ -1482,16 +1493,21 @@ 通常は最大で15分ほどかかります Tangemカードのセットアップ カードを発行しています - カードを準備しています。少し時間がかかる場合があります。 + カードは通常、5分以内に自動で発行されます。手動での審査が必要な稀な場合は、最大48時間かかることがあります。 Tangem Pay 中止を確定する KYC手続きを中止しますか?いつでも再開できます。 プロフィールを確認できませんでした。ご不明な点があればサポートまでお問い合わせください。 申し訳ございませんが、本人確認を行うことができませんでした + KYCの認証に失敗しました KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 - 以下のボタンから、現在のKYCステータスを確認するか、KYCをキャンセルできます。 + 書類は通常、5分以内に自動で確認されます。手動での審査が必要な稀な場合は、最大48時間かかることがあります。 + KYCが承認されませんでした + KYCブロックを非表示 + 申し訳ありません、確認できませんでした + あなたのプロフィール。 無料のTangem Visaバーチャルカードを入手 日常の支払いにUSDCを利用 カードをGET @@ -1508,7 +1524,9 @@ 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 - 同期が必要です + \nPINコードの設定 + 未同期 + アクセスを復元 日常の支払いにUSDCを利用 Tangem Payは現在一時的に利用できません。 Tangem Pay @@ -1693,7 +1711,7 @@ リカバリーフレーズを使って既存のウォレットをインポートします。 ウォレットをインポート リカバリーフレーズを入力 - ウォレットのバックアップが正常に完了しました。 + ウォレットのインポートに成功しました。 ウォレットをインポート インポート完了 ウォレットをインポート @@ -1708,7 +1726,7 @@ このアクセスコードはウォレットを保護し、ログインや取引の署名に使用されます。 アクセスコードの設定・変更 アクセスコードを変更する - ウォレットの受信取引とTangemの更新について通知を受け取る。 + ウォレットへの入金取引をプッシュ通知で受け取る。 現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。 取引通知 アクセスコードを設定する @@ -1717,7 +1735,7 @@ %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 許可付与のプロセスは現在進行中であり、まもなく完了する予定です。 承認中 - カードまたはリングのアクティベーションが正しく完了しませんでした。デバイスのNFCモジュールに問題があるか、カードまたはリングをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 + アクティベーションが正常に完了しませんでした。NFCの問題、またはタップ方法が正しくない可能性があります。サポートチームまでお問い合わせください。 アクティベーションに失敗しました 2024年12月3日、BEP-2ネットワークはネットワーク開発者の決定により使用不能となり、サポートは終了しました。 BNBビーコンチェーンは閉鎖されました @@ -1728,6 +1746,8 @@ はい、わかりました! すごくクールだ! リフレッシュ + Cloreの公式ドキュメントによると、12月21日以前に受け取ったすべてのコインは Clore(ERC-20トークン)へ移行されますが、同日以降に受け取ったコインは移行されません。送金(移行)ソリューションは現在準備中です。続報をお待ちください。 + Cloreネットワークの移行 現在デモモードです デモモードが有効になっています スキャンしたカードは開発者カードです。ウォレットの作成には使用しないでください。 @@ -1775,7 +1795,7 @@ 現在、ネットワークにアクセスできません。しばらくしてからもう一度お試しください。 ネットワークにアクセスできません ウォレットに入金 - ウォレットがバックアップされていません。アセットを保護するために今すぐこの手順を実行してください。 + ウォレットはまだバックアップされていません。今すぐバックアップして資産を保護しましょう。 バックアップがありません このカードは以前取引に使用されたことがあります。信頼できない出所から受け取った場合は、全資金を引き出すことを検討してください。あなたのカードであれば、何もする必要はありません。 カードはすでに取引に署名済みです @@ -1914,10 +1934,10 @@ Tangemハードウェアウォレットを使用する 詳細を見る・購入する 破棄 - バックアップが中断されました。再開しますか? + バックアップが中断されました。再開しますか? はい、再開します 破棄 - バックアップを破棄する場合は、デバイスを工場出荷時の設定にリセットしてやり直す必要があります。 + 今バックアップを破棄すると、デバイスを初期化して最初からやり直す必要があります。 バックアップを再開する これはやり直しができないアクションです %sでログイン diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ee6d3b99d7..20c4204701 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -9,6 +9,8 @@ Код доступа может быть легко угадан Введите код доступа Неверный код доступа. Ваш мобильный кошелёк будет удалён после ещё %s неверных попыток. + Неверный код доступа. Приложение будет заблокировано после ещё %s неудачных попыток. + Неверный код доступа.\nПожалуйста, подождите %s секунд и попробуйте снова. Подтвердите ваш код доступа, чтобы продолжить. Повторите код доступа Установите %s-значный код доступа для разблокировки кошелька. @@ -37,10 +39,12 @@ Невозможно отредактировать аккаунт Аккаунт сохранен Аккаунт для наград + Аккаунт #%s — используется для генерации адреса. Добавить аккаунт Сохранить Название аккаунта - Имя аккаунта уже существует + Аккаунт с таким названием уже существует. Пожалуйста, выберите другое название. + Название аккаунта уже используется Аккаунт Новый аккаунт Добавить аккаунт @@ -252,6 +256,7 @@ Подтвердить Подключение Обратиться в поддержку + Связаться с поддержкой Продолжить Конвертировать Копировать @@ -377,6 +382,7 @@ Поддерживаемые сети Обменять Tangem + Tangem Кошелек условия участия Условиями использования На @@ -544,6 +550,10 @@ ID: %s ID транзакции скопирован Обменяйте любой актив в вашем портфеле на этот токен + Более высокая скорость означает более быстрое подтверждение, но и более высокую сетевую комиссию. %s + Выбрать скорость + Выберите какой токен будет использоваться для оплаты комиссии сети. %s + Выбрать токен Рынок и Новости В тренде Информация ниже не является обязательной. Вы можете стереть её, если хотите. @@ -600,7 +610,7 @@ Создать новый кошелек Повысьте уровень безопасности с помощью продвинутого аппаратного кошелька Tangem. Аппаратный кошелёк - Перенести текущий кошелёк в Tangem Wallet. + Перенесите свой текущий кошелек в холодный кошелек Tangem. Улучшить текущий кошелёк Перейти к бэкапу Пожалуйста, создайте резервную копию вашего кошелька перед установкой кода доступа. @@ -625,7 +635,7 @@ Вы уверены, что хотите выполнить эту операцию? Забыть кошелек Перейти к резервному копированию - Посмотреть резервное копирование + Показать фразу восстановления Забыть кошелек Забыть всё равно Резервная копия этого кошелька существует. Проверьте её перед удалением, чтобы убедиться, что сможете восстановить кошелёк позже. @@ -633,6 +643,7 @@ Забыть этот кошелек? Я понимаю, что если я не создал резервную копию кошелька перед его удалением, я могу потерять к нему доступ. Я понимаю, что удаление моего кошелька не стирает его — оно просто удаляет его с моего устройства. + Апгрейд Фраза восстановления больше не нужна — ваша карта или кольцо Tangem становятся вашей надёжной резервной копией. Резервное копирование Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек. @@ -814,6 +825,23 @@ Обновитесь до версии %1$s, чтобы создать мобильный кошелёк Мобильный кошелек требует %1$s или новее. Все новости + Нравится + + %dч назад + %dч назад + %dч назад + %dч назад + + + %dмин назад + %dмин назад + %dмин назад + %dмин назад + + Резюме + Связанные новости + Связанные токены + Связанные новости Будьте в курсе Функция NFC недоступна на вашем устройстве О NFT @@ -1100,6 +1128,7 @@ Сбросить карту Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку Я понимаю, что не смогу использовать эту карту для восстановления кода доступа на других картах текущего кошелька + Я понимаю, что полностью потеряю доступ к своей карте Tangem Pay и всем средствам на ней без возможности восстановления. Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты или кольца. Вы не сможете восстановить текущий кошелек или использовать данную карту или кольцо для восстановления кода доступа. Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты или кольца. Вы не сможете восстановить текущий кошелек. Все Tangem девайсы были сброшены. @@ -1446,6 +1475,7 @@ Карта заморожена Обратиться в поддержку Другое + Невозможно использовать на устройствах с root-доступом. Успешно завершено Отклонено В процессе @@ -1459,6 +1489,7 @@ Карта разморожена Вывести Запрещено использовать на root-устройствах + Отменить KYC Пополнить Способы пополнения Номер @@ -1512,14 +1543,17 @@ Обычно это занимает до 15 минут Готовим вашу Tangem Card Выпускаем карту - Выпускаем карту, это займет немного времени. + Обычно это занимает не больше 5 минут, в редких случах при необходимости ручной проверки – до 48 часов. Tangem Pay + Подтвердите отмену + Если вы отмените KYC сейчас, вы сможете потом начать его заново Не удалось подтвердить ваш профиль. Если у вас есть вопросы, обратитесь в службу поддержки. К сожалению, нам не удалось подтвердить вашу личность KYC в процессе Посмотреть статус KYC в процессе для Tangem Pay - Вы можете посмотреть текущий статус KYC или отменить его + Обычно документы проверяются автоматически в течение 5 минут, в редких случах при необходимости ручной проверки – до 48 часов. + KYC отклонено Откройте бесплатную виртуальную карту Tangem Visa Оплачивайте ежедневные покупки в USDC Открыть карту @@ -1639,7 +1673,9 @@ Установить ПИН-код ПИН не принят. Попробуйте ещё раз или введите другой код. Слабый ПИН: не используйте повторы или последовательности. + Код валюты Разблокировать + Отсканируйте свою карту, чтобы разблокировать доступ Выберите тип кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. Создать аппаратный кошелёк @@ -1706,7 +1742,7 @@ Импортируйте существующий кошелек через вашу фразу восстановления. Имрортировать кошелек Ввести фразу восстановления - Вы успешно сделали бэкап вашего кошелька. + Вы успешно импортировали свой кошелек. Импортировать кошелек Импорт завершен Импортировать кошелек @@ -1720,8 +1756,8 @@ Откройте Tangem Wallet Код доступа защищает кошелёк и нужен для входа и подписания транзакций. Установить/изменить код доступа - Изменить Код Доступа - Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem. + Изменить код доступа + Получайте push-уведомления о входящих транзакциях в кошелек. В настоящий момент push-уведомления могут не работать на устройствах Huawei. Мы уже работаем над решением этой проблемы и планируем исправление в ближайших обновлениях. Спасибо за понимание! Уведомления о транзакциях Установить Код Доступа @@ -1761,6 +1797,7 @@ Сумма получения не может быть менее %s Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна + Предупреждающий список FCA Сервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s 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 6df04c2899..5954c2933e 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1,5 +1,67 @@ + Ваш гаманець не захищений без коду доступу. + Все одно пропустити + Код доступу не встановлено + Змінити код + Ваш код доступу буде використовуватися для розблокування гаманця та захисту доступу до ваших активів. + Використати все одно + Код доступу може бути легко вгаданий + Введіть код доступу + Неправильний код доступу. Ваш мобільний гаманець буде видалено після ще %s невірних спроб. + Неправильний код доступу. Додаток буде заблоковано після ще %s невдалих спроб + Неправильний код доступу.\nБудь ласка, зачекайте %s секунд і спробуйте ще раз. + Підтвердьте свій код доступу, щоб продовжити + Повторно введіть код доступу + Встановіть %s-значний код доступу для розблокування вашого гаманця. + Створити код доступу + Код доступу + Не можна створити більше ніж %1$s акаунтів. Архівуйте, щоб додати новий. + Неможливо додати новий акаунт + Акаунт архівовано + Архівовані акаунти + Відновити + Ви збираєтеся відновити \"%1$s\". + Відновити акаунт + Ви досягли ліміту в 20 активних акаунтів. Архівуйте акаунт, щоб відновити цей. + Неможливо відновити акаунт + Архівовано + Не вдалося архівувати акаунт. Будь ласка, спробуйте пізніше. + Цей акаунт бере участь у реферальній програмі. + Цей акаунт не можна архівувати + Не вдалося створити акаунт. Будь ласка, спробуйте пізніше. + Акаунт створено + Архівний акаунт + Архів + Ви архівуєте цей акаунт, але ви завжди можете його відновити. + Архівування... + Акаунт + Неможливо відредагувати обліковий запис + Акаунт збережено + Акаунт для винагород + Акаунт №%s — використовується для деривації. + Додати акаунт + Зберегти + Назва акаунту + Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву. + Акаунт + Новий акаунт + Додати акаунт + Редагувати акаунт + Спробуйте пізніше. Якщо проблема повториться, зверніться у підтримку — ми допоможемо її вирішити. + %1$s в %2$s + Основний акаунт + Ви перевищили ліміт %1$s активних акаунтів. Архівуйте, щоб продовжити + Неможливо відновити акаунт + Акаунт відновлено + Натисніть та утримуйте акаунт, щоб змінити порядок акаунтів + Продовжити + Скасувати + Ви впевнені, що хочете скасувати створення нового акаунту? + Ви впевнені, що хочете скасувати зміни? + Незбережені зміни + Деякі користувацькі токени будуть автоматично переміщені з \"%1$s\" до \"%2$s\", оскільки їх похідні належать до того акаунту. + Деякі користувацькі токени будуть автоматично переміщені Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для покупки Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для продажу Продати @@ -34,12 +96,19 @@ Токени в мережі %1$s не підтримуються цією карткою або кільцем через обмеження прошивки. У вас виникли труднощі зі скануванням картки або кільця? Ця картка не призначена для роботи з цим додатком + Спочатку встановіть код доступу, щоб увімкнути біометрію. + Використовуйте %1$s для розблокування гаманця і підтвердження конфіденційних дій, таких як підписання транзакцій. Для апаратних гаманців для підпису все ще потрібна картка або кільце. Комісія за замовчуванням Увімкніть комісію за замовчуванням, щоб автоматично встановлювати комісію за транзакцію і пропускати сторінку комісій під час відправлення коштів. Ви завжди можете повернутися до цієї сторінки за необхідності. Перейдіть до налаштувань, щоб увімкнути біометричну автентифікацію в додатку Tangem Увімкнути біометричну автентифікацію + Вимкнення %1$s вимагатиме введення коду доступу для розблокування додатку та взаємодії з гаманцем. + Пізніше буде необхідно ввести код доступу для безпечного зберігання гаманця. Усі збережені коди доступу будуть видалені. Вам доведеться вводити код доступу при роботі з гаманцем. При відключенні функції збереження гаманця всі до цього збережені гаманці будуть видалені зі застосунку. + Це призведе до видалення всіх збережених кодів доступу до гаманця. Вам потрібно буде ввести код доступу знову, щоб користуватися гаманцем. + Необхідний код доступу + Ця опція вимикає можливість використовувати біометрію для конфіденційних дій. Вам потрібно буде вводити код доступу щоразу, коли ви підписуєте транзакцію. Зберегти код доступу Підключіть функцію збереження кодів доступу від карток на телефоні у зашифрованому виді, і при роботі з карткою або кільцем замість коду доступу буде запитуватися біометрична автентифікація. Зберігати гаманець у додатку @@ -49,6 +118,16 @@ Системна Тема Налаштування застосунку + Додати гаманець + Виберіть гаманець для входу + З поверненням! + + %s картка + %s картки + %s карток + %s картки + + Мобільний гаманець Ви успішно створили резервну копію свого гаманця. Ці слова неможливо відновити у разі втрати. Зберігайте їх надійно. Бекап завершено @@ -68,10 +147,17 @@ Баланси приховані Згідно інформації від розробників мережі, токени Kaspa знаходяться у режимі бета. Слідкуйте за оновленнями! Бета режим + Біометрія на вашому пристрої вимкнена, тому ви не можете використовувати її для розблокування гаманців. Увімкніть біометрію в налаштуваннях пристрою, щоб знову використовувати цей метод. + Біометрична автентифікація відключена Будь ласка, відскануйте картку або кільце + Ви досягли ліміту спроб біометрії. Розблокуйте гаманець за допомогою дотику пристрою або введіть код доступу. + Біометрична автентифікація заблокована Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце + Біометричний вхід тимчасово заблоковано. Будь ласка, повторіть спробу через 30 секунд або розблокуйте гаманець дотиком пристрою чи кодом доступу. Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Біометричні дані на вашому пристрої було оновлено. Будь ласка, оберіть свій гаманець і введіть код доступу до нього, щоб знову увімкнути біометричний вхід. + Увага Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше. Помилка активації Ваш промокод було успішно активовано. Винагороду буде зараховано на ваш рахунок протягом 14 днів. @@ -90,6 +176,19 @@ %d пристроїв %d пристроїв + + %1$d токен + %1$d токени + %1$d токенів + %1$d токени + + Будь ласка, скиньте налаштування наступного пристрою, щоб продовжити. + Скидання гаманця + Всі пристрої Tangem було скинуто. Тепер ви можете продовжити оновлення гаманця. + Оновити ще раз + Скидання завершено + Ми рекомендуємо завершити процес скидання для всіх пристроїв Tangem у цьому гаманці. + Деякі пристрої Tangem все ще потрібно скинути. Вимкніть цю опцію, якщо не хочете, щоб ця картка використовувалася для скидання кодів доступу на інших картках у цьому гаманці. Зверніть увагу, скидання коду доступу також не буде доступним на цій картці. Дозволяє використовувати цю картку для скидання коду доступу на інших картках у цьому гаманці Відновлення коду доступу @@ -115,19 +214,25 @@ Недостатньо ADA Прийняти Доступ заборонено + Акаунт + Акаунти + Активувати Додати Додати у портфель Додати токен + Додано Адреса Усе Дозволити Сума Аналітика + і Застосовувати Затвердження Підтвердити Увага Доступні мережі + Резервне копіювання Баланс: %s Баланс біометричну автентифікацію @@ -138,15 +243,20 @@ Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності Скасувати Змінити + Оберіть акаунт Виберіть дію + Оберіть мережу Оберіть токен Оберіть гаманець Отримати Отримати винагороди Закрити + Незабаром Підтвердити Підключення Звернутися у підтримку + Звернутися до служби підтримки Tangem + Звернутися до служби підтримки Visa Продовжити Конвертувати Копіювати @@ -167,6 +277,7 @@ днів Видалити + Вимкнути Вимкнуто Від\'єднати Готово @@ -186,8 +297,13 @@ Повільно Швидкість та комісія Завершити + Забути + Безкоштовно + З + З %s Синхронізувати адреси Почати + Отримати токен Перейти до провайдера Перейти до токену Зrozуміло @@ -198,16 +314,27 @@ Пізніше Дізнатися більше Залишилося %1$s + Legacy Bitcoin Заблокований + Заблоковані гаманці Основна мережа місяць Комісія мережі Сума відправлення буде зменшена на %1$s (%2$s) для покриття обраного рівня комісії + + %1$s мережа + %1$s мережі + %1$s мереж + %1$s мережі + + Нова адреса + Новини Далі NFT Ні Немає адреси Не додано + Не зараз Зараз ОК Відкрити в браузері @@ -220,10 +347,12 @@ %1$s — %2$s Детальніше Отримати + Рекомендовано Відхилити Оновити Перейменувати Обов\'язково + Скинути Зберегти Зберегти зміни Пошук @@ -238,19 +367,25 @@ Сервер недоступний, спробуйте пізніше Поширити Поділитися посиланням + Сховати Показати більше Підписати Підписати та надіслати + Пропустити + Щось пішло не так Застейкати Стейкінг Почати Продовжити Успіх Підтримка + Підтримувані мережі Обмін + Tangem умови участі Умовами використання До + На %s Сьогодні %d токен @@ -262,17 +397,23 @@ Статус транзакції Транзакції Переказ + Неможливо завантажити дані... Я зрозумів + Я розумію, продовжити Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно Скасувати стейкінг Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. + Скопійовано тиждень з Так Режим дохідності Адреса контракту скопійована! Доступні мережі + Деривація вашого токена співпадає с деривацією %1$s. Ваш токен буде додано до цього акаунта. + Деривація належить іншому акаунту. + Токен додано до акаунту %1$s Адреса контракту Адреса контракту недійсна Будь ласка, оберіть мережу @@ -313,6 +454,7 @@ Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси %s хешів Номер пристрою + Відкрити чат підтримки Додати більше карток Валюта застосунку Приховувати баланси жестом перевороту @@ -333,11 +475,13 @@ Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. + Обміняйте будь-який актив у вашому портфелі на цей токен Мої токени У вас немає доданих токенів. Додайте токени для обміну Недоступний для обміну з %s Послугу надає Статус + Виберіть провайдера Tangem пропонує обмін токенів через сторонніх провайдерів відповідно до умов кожного провайдера Провайдер Виникла помилка. Код: %s @@ -388,6 +532,7 @@ Найкращий курс Список попереджень FCA Найкращий вибір + Список попереджень FCA Доступно до %s Доступно від %s Недоступно для цієї пари @@ -399,10 +544,15 @@ Приховати цю транзакцію Після приховування статус транзакції більше не можна буде переглянути. Замість цього ви можете просто провести пальцем, щоб закрити. Приховати статус транзакції? + Цей токен не підтримується. Будь ласка, виберіть інший токен для обміну. + %s не підтримується + Обміняти з Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано Обміняйте будь-який актив у своєму портфелі на цей токен + Ринок та Новини + В тренді Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. Розкажіть, яких функцій вам не вистачає, і ми спробуємо вам допомогти. Розкажіть, будь ласка, яку картку або кільце ви маєте? @@ -434,9 +584,11 @@ Генерація ключа Операції з криптографією відбуваються всередині захищеного чіпу, стійкого до клонування та фізичному взлому. Безпека на апаратному рівні + Додати існуючий гаманець Створити новий гаманець Купити Сканувати + Дозволити додатку \"Tangem\" використовувати біометричну автентифікацію для підтвердження вашої особи та відкриття додатку? в %s В мережі %s Ви впевнені, що хочете скасувати процес створення коду доступу? @@ -455,7 +607,7 @@ Створити новий гаманець Підвищіть рівень своєї безпеки за допомогою просунутого апаратного гаманця Tangem. Апаратний гаманець - Перенесіть свій поточний гаманець у Tangem. + Перетворіть поточний гаманець на холодний гаманець Tangem. Оновіть поточний гаманець Перейти до бекапу Будь ласка, створіть резервну копію свого гаманця, перш ніж створювати код доступу. @@ -488,6 +640,7 @@ Забути цей гаманець? Я розумію, що якщо я не створив резервну копію гаманця перед його видаленням, я можу втратити досту до нього. Я розумію, що видалення мого гаманця не видаляє його повністю, а просто видаляє його з мого пристрою. + Оновлення Фраза відновлення більше не потрібна — ваша картка або кільце Tangem стає вашою безпечною резервною копією. Резервне копіювання з Tangem Цей пристрій не може бути використаний для оновлення, він вже містить інший гаманець. @@ -554,17 +707,21 @@ Обраний токен наразі недоступний для дій у криптогаманці. Але не хвилюйтеся, ви можете висловити свою зацікавленість, проголосувавши за його інтеграцію. Проголосувати Гаманець не підтримує більше однієї мережі + Про монету Щоб купити, обміняти або отримати цей актив, додайте його до свого портфоліо Цей актив наразі не підтримується в гаманці Цей токен недоступний для цього гаманця Додати + APY %s Моє портфоліо Маркет + Заробляйте з Tangem Щоб згенерувати адреси для обраних мереж, відскануйте вашу картку або кільце Tangem гаманця Щоб додати токени, потягніть вгору або торкніться панелі пошуку Дані цього розділу отримані з наступних мереж: %s Не вдалося завантажити дані... Немає даних + Пульс ринку Швидкі дії Шукати на маркеті Результат @@ -586,8 +743,10 @@ Лідери росту Лідери падіння В тренді + Режим дохідності Стейкінг — простий спосіб отримувати дохід з вашої криптовалюти. %s Отримуйте до %s APY + Токен додано Про %s %d біржа @@ -658,6 +817,25 @@ Обсяг Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени + Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s + Активувати режим дохідності + Оновіться до версії %1$s, щоб створити мобільний гаманець + Мобільний гаманець потребує %1$s або новіше. + Всі новини + + %1$s годину тому + %1$s години тому + %1$s годин тому + %1$s годин тому + + + %1$s хвилину тому + %1$s хвилини тому + %1$s хвилин тому + %1$s хвилин тому + + Пов\'язані токени + Залишайтеся в курсі подій Функція NFC недоступна на вашому пристрої Про NFT NFT @@ -716,6 +894,11 @@ Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. Для створення акаунту надішліть кошти на цю адресу Акаунт одержувача не містить трастлайну для активу, що надсилається. + Отримайте $10 в BTC на кожен гаманець. Поспішайте! + Чорна п\'ятниця: знижки до 30% + Купити + Час обмежений! + 1+1: Купи один гаманець — отримай 50% знижку Приєднатися Поділіться промокодом — заробіть 5 USDT з кожної покупки. Ваші друзі отримають знижку 10% на картку Tangem! Отримуй бонуси за кожного друга! @@ -762,12 +945,14 @@ Генеруйте ключі приватно Ваша картка активована та готова до використання Успішно! + Ваш гаманець налаштований і готовий до використання! У цьому випадку вам доведеться почати наново. Ви хочете вийти з процесу активації? Підготовка На картці, яку ви намагаєтеся додати, вже створено інший гаманець. Якщо у вас є кошти на цьому гаманці, будь ласка, виведіть їх, а потім скиньте цю картку до заводських налаштувань і додайте її як резервну. Збережіть ваш гаманець Резервна копія + Біометрія Дізнатися більше про seed-фразу @@ -825,6 +1010,7 @@ Відновлення коду доступу Ідентичні картки Код доступу + Усі пропозиції Доступно з %s Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами Пошук за країною @@ -832,13 +1018,28 @@ Інші валюти Популярні фіати Пошук по валюті + Ця транзакція вже була оброблена. Додаткові дії не потребуються. + Шукаємо найвигідніший курс... + Миттєво Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s + Послуга надається зовнішнім провайдером.\nTangem не несе відповідальності. Сума покупки не може бути більше ніж %s Сума покупки повинна бути не менше %s Для данної валюти немає доступних провайдерів + Найшвидший Оплата з + Спосіб оплати Доступно до %s Доступно від %s + + %s провайдер + %s провайдери + %s провайдерів + %s провайдерів + + Провайдери + Нещодавно використано + Рекомендовано Ви зможете завершити транзакцію у стороннього постачальника, %s Перенаправлення на %s... Наші сервіси недоступні в цій країні @@ -847,8 +1048,21 @@ Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. Параметри Ви можете закрити цей екран і перевірити статус транзакції на екрані деталей токена. + + до %s дня + до %s днів + до %s днів + до %s днів + + %s хв + Доступно з + Доступно до + Ви отримаєте + Послуга надається зовнішнім провайдером. \nTangem не несе відповідальності. Ви можете перевірити статус транзакції на сторінці токена + До Через + Ви заплатите Групами За балансом Сортування токенів @@ -860,6 +1074,7 @@ Налаштування Увімкнути сповіщення Отримуйте сповіщення про вхідні транзакції в підтримуваних мережах. + Сповіщення про транзакції Виберіть з галереї Налаштування Ви не надали доступ до своєї камери @@ -871,6 +1086,7 @@ Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. Надсилайте лише %1$s у мережі %2$s Переказуйте кошти з будь-якого гаманця або біржі + Адреса для отримання винагород Взяти участь Не вдалося завантажити інформацію по реферальній програмі. Будь ласка, спробуйте пізніше. Не вдалося завантажити інформацію по реферальній програмі. Код помилки: %s. Будь ласка, спробуйте пізніше. @@ -909,8 +1125,14 @@ Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець. + Усі пристрої Tangem було скинуто. + Щось пішло не так у процесі активації. Будь ласка, скиньте картки по черзі. + Не вдалося перевірити картку + Будь ласка, скиньте наступний пристрій, щоб продовжити Власникам кілець - 3 обміни без комісії на Changelly до 15.11! Обмін з 0% комісією! + Пристрої з root-доступом вважаються менш безпечними. Ваші дані можуть бути піддані додатковим ризикам. + Виявлено root-доступ Увійдіть у додаток та слідкуйте за своїм балансом без сканування картки чи кільця Доступ до додатку Використовувати біометрію @@ -938,6 +1160,7 @@ Транзакція не завершена Конвертувати в інший токен Сума + Буде надіслано одержувачу Ви можете встановити розмір комісії за транзакцію, налаштувавши значення в полі Satoshi per vByte. Комісія, яка буде стягнута за вашу транзакцію. Ви можете встановити власне значення. Максимальна комісія @@ -962,6 +1185,7 @@ Перевірте підключення до мережі Інформація щодо комісії в мережі недоступна Ви надсилаєте + З %s Ліміт газу Це максимальна кількість газу, яка буде витрачена на здійснення транзакції чи контракту. Ліміт газу запобігає несподіваним або необмеженим стягненням під час виконання транзакції. Вартість газу @@ -969,6 +1193,7 @@ Все Максимальна сума Комісія може сягати до + Мемо: %s Недопустимий Memo Покриття мережевої комісії Nonce @@ -1018,6 +1243,7 @@ комісія мережі буде покрита, використовуючи %1$s енергію Комісія мережі буде зменшена, за рахунок використання %1$s енергії включно з комісією мережі %1$s + На адресу Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Сума перевищує залишок @@ -1047,14 +1273,21 @@ Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я Змусьте свій токен працювати + Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні. + Щоб почати стейкінг, ваш TON-акаунт потрібно активувати транзакцією на 1 TON. Кошти залишаються на вашому акаунті, оскільки цей крок лише активує його для стейкінгу. + Активація акаунту + Мережева комісія змінилася. Будь ласка, перевірте нову суму перед тим, як продовжити. + Комісія мережі оновлена Сума для стейкінгу має бути не менше %s Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. + APR %1$s%% Зняти кошти Комісія за стейкінг-акаунт Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. Процентна ставка APR: Показує відсоток, який ви можете заробити за рік без урахування складного відсотка. Нараховані відсотки не додаються до вашого балансу, тому на них не нараховуються додаткові відсотки. + Річна відсоткова дохідність APY: Показує загальний відсоток, який ви можете заробити за рік з урахуванням складного відсотка. Складний відсоток означає, що нараховані відсотки додаються до вашого балансу, і на них також нараховуються відсотки. APR APY @@ -1083,6 +1316,7 @@ Комісія за схвалення токену, щоб верифікувати, що саме ви дозволяєте використання токена для стейкінгу. Використовуючи функцію стейкінгу, ви погоджуєтесь з %1$s та %2$s Заблоковано + Максимальна сума: %s Перемістити Нативний стейкінг На даний момент немає доступних активних валідаторів для стейкінгу. Будь ласка, спробуйте пізніше. @@ -1113,6 +1347,7 @@ Рестейк дозволяє вам перемістити ваші кошти від одного валідатора до іншого без необхідності виводити кошти зі стейкінгу Ви збираєтеся застейкати весь свій баланс. Ми рекомендуємо залишити невелику суму, щоб покрити комісію мережі за зняття коштів або отримання винагороди. Щоб розпочати стейкінг, спочатку активуйте свій TON-акаунт. + Активація акаунту Щоб почати стейкінг в TON, спочатку здійсніть вихідну транзакцію на будь-яку суму — це активує ваш гаманець. До 0.2 TON може знадобитися додатково мережевої комісії для завершення транзакції. Невикористана частина буде повернута. Для виконання операції потребується додатково 0.2 TON, крім мережевої комісії. Будь ласка, поповніть баланс. @@ -1186,6 +1421,7 @@ Для відправки потрібна вхідна транзакція на суму не менше %1$s Недостатньо коштів Підтверджуючи, ви дозволяєте смартконтракту використовувати ваші токени в майбутніх транзакціях. + Фіксований курс Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! @@ -1210,33 +1446,130 @@ Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки. Ви обмінюєте Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. + Високий вплив на ціну Недостатньо коштів Надати дозвіл Обміняти Ви отримаєте Оберіть токен недоступно + Будемо раді вашому зворотньому звʼязку + Tangem Pay у режимі beta + Картку заморожено + Оплата карткою + Депозит + Оскаржити + Переглянути транзакцію + Сервісна комісія + Комісія + Зберігайте свої гроші в безпеці. Ви можете розморозити картку у будь-який час. + Заморозити вашу картку? + Не вдалося заморозити картку. Спробуйте пізніше. + Заморозити + Вашу картку заморожено. + Звернутися до підтримки + Інше + Завершено + Відхилено + В очікуванні + Умови, комісії та ліміти + Умови та обмеження + Банк відхилив цей запит на транзакцію. + Ця комісія покриває витрати на обробку вашого переказу. + Продовжуйте користуватися карткою. Заморозити можна в будь-який момент. + Розморозити картку? + Не вдалося розморозити картку. Спробуйте пізніше. + Картку розморожено. + Виведення коштів + Заборонено використовувати на root-пристроях + Скасувати KYC + Поповнити рахунок + Варіанти поповнення Номер картки + Змінити PIN-код + Картка готова до оплат. + PIN-код створено CVC + Не вдалося завантажити дані. Спробуйте пізніше. Термін дії + Заморозити картку Сховати деталі + Приховати + Відкрити Google Wallet + Налаштуйте Tangem Pay в кілька кроків і почніть платити за допомогою Google Pay. + Налаштуйте Tangem Pay в кілька кроків і почніть платити за допомогою Apple Pay. + Додайте картку до Google Pay Додайте свою картку в Apple Pay + Відкрийте Google Wallet Натисніть кнопку \"+\" у верхньому правому куті Відкрити Apple Wallet + Натисніть «Додати картку» Натисніть «Дебетова або кредитна картка» + Введіть дані картки вручну + Підтвердьте картку за допомогою одноразового коду, надісланого на ваш пристрій. Все готово! Ваша картка готова до використання. + Додайте картку до Google Pay Додайте свою картку в Apple Pay + ПІН-код + Поділіться своєю адресою або покажіть QR-код + Виявлено технічні проблеми. Будь ласка, спробуйте пізніше або зверніться до служби підтримки. + Поповнення наразі недоступне + Показати Показати деталі Обміняйте будь-який актив у вашому портфелі на картку + Реквізити картки Розморозити картку + Ваш ПІН + Вивести + Вивід наразі недоступний + Ви не можете розпочати обмін або новий вивід, поки не завершено поточний. + Вивід виконується + Змінити PIN-код + Можна подивитися тут, якщо забудете його. + Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення. + Не вдалося випустити картку + Технічна помилка, спробуйте ще раз, натиснувши кнопку нижче + Технічна помилка, звʼяжіться з підтримки Отримайте безкоштовну віртуальну картку Tangem Visa + Отримати Tangem Pay + Написати у підтримку + Зазвичай це займає до 15 хвилин + Готуємо вашу картку Tangem + Випускаємо картку + Карту зазвичай випускають автоматично протягом 5 хвилин. У рідкісних випадках, коли потрібна ручна перевірка, процес може тривати до 48 годин. + Tangem Pay + Підтвердити скасування + Ви впевнені, що хочете зупинити процес KYC? Ви можете повернутися до нього в будь-який момент. + Не вдалося підтвердити ваш профіль. Якщо у вас є питання, зверніться у службу підтримки. + На жаль, нам не вдалося підтвердити вашу особу + KYC в процесі + Переглянути статус + Перевірка KYC для Tangem Pay в процесі + Документи зазвичай перевіряються автоматично протягом 5 хвилин. У рідкісних випадках, коли потрібна ручна перевірка, це може зайняти до 48 годин. Отримайте безкоштовну віртуальну картку Tangem Visa Використовуйте USDC для щоденних платежів + Отримати картку + Віртуальну картку можна додати до Apple Pay та Google Pay + Витрачайте свої активи будь-де + Немає жодних додаткових комісій за покупки + Платіть стільки, скільки бачите + Буде створено окремий платіжний рахунок без розкриття ваших адрес та активів + Неперевершена конфіденційність + Отримайте безкоштовну картку Tangem Pay за лічені хвилини + Платіжний акаунт Платіжний рахунок не синхронізовано + Слабкий ПІН: не використовуйте повторів або послідовностей. Ми усуваємо технічну проблему. Будь ласка, спробуйте пізніше. Сервіс тимчасово недоступний + Не можемо показати дані картки, але оплати продовжують працювати. + Встановіть \nPIN-код + Не синхронізовано + Відновити доступ Використовуйте USDC для щоденних платежів + Tangem Pay тимчасово недоступний + Tangem Pay Натисніть кнопку нижче, щоб відновити доступ + Ваш PIN-код Це мій гаманець Баланси приховано Баланси показано @@ -1265,6 +1598,7 @@ Токен у мережі %%image%% %1$s Токен %1$s (%2$s) є основною валютою в мережі %3$s і не може бути прихований до тих пір, поки у вас в списку є інші токени цієї мережі Неможливо приховати %s + Показати QR-код Обмінюйте цей токен на інші з %1$s комісією за обслуговування з %2$s по %3$s лютого. Обмін із Changelly, %s комісії Обміняти @@ -1279,6 +1613,7 @@ Кілька адрес Історія транзакцій наразі не підтримується для цього блокчейну. Але не хвилюйтеся, ми працюємо над цим! А поки можете перевірити це в оглядачі. Операція + для: %s від: %s до: %s валідатор: %s @@ -1301,6 +1636,7 @@ Ця дія є незворотною. Ви не матимете доступу до старого гаманця. Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції Спробуйте ще раз пізніше. Якщо проблема залишиться, зверніться до служби підтримки. + Щось пішло не так! Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. @@ -1312,6 +1648,8 @@ Ранній доступ до нових функцій та ексклюзивних пропозицій. Нові функції та важливі новини Бажаєте використовувати Push-повідомлення? + Увімкніть push-сповіщення, щоб отримувати повідомлення, коли кошти надходять на ваш гаманець. + Не пропустіть транзакцію Додати новий гаманець Якщо ви видалите цей гаманець без резервної копії, ви назавжди втратите доступ до своїх коштів. Ви впевнені, що хочете видалити цей гаманець? @@ -1322,7 +1660,39 @@ Перейменування гаманця Розблокувати все Розблокувати все з %s + + доступно протягом %1$s дня + доступно протягом %1$s днів + доступно протягом %1$s днів + доступно протягом %1$s днів + + Баланси та ліміти + Код доступу + Активація акаунту + Ви впевнені, що хочете вийти? Ви можете продовжити пізніше з того місця, де зупинилися. + Це не займе багато часу. Ми налаштовуємо ваш акаунт. + Це не займе багато часу. Ми завершуємо активацію. + Встановіть 4-значний код.\nВін буде використаний для платежів. + ПІН-код + Встановити ПІН-код PIN-код не прийнятий. Спробуйте ще раз або введіть інший код. + Слабкий ПІН: не використовуйте повторення або послідовності + Перейти на веб-сайт + Давайте продовжимо налаштування вашого акаунту. + З поверненням! + Виконайте ці кроки, щоб налаштувати свій акаунт. + Вітаємо! + Оскаржити цю транзакцію + Розблокувати + Відскануйте картку, щоб розблокувати доступ + Потрібне розблокування + Оберіть тип гаманця + Відскануйте картку або кільце Tangem, щоб відновити або імпортувати з іншого гаманця. + Створити апаратний гаманець + Хочете придбати гаманець Tangem? + Імпортувати фразу відновлення + Відновіть гаманець на телефоні або імпортуйте з іншого додатку — зручно, але менш безпечно, ніж картка Tangem. + Що обрати? Блокчейн недоступний. Спробуйте пізніше Відскануйте картку або кільце Цей гаманець вже був активований раніше.\nЯкщо це було зроблено не вами, зверніться до служби підтримки.\nTangem ніколи не продає гаманці разом з попередньо згенерованим кодом доступу. @@ -1343,6 +1713,8 @@ Невірна картка або кільце обрана в додатку Tangem Не вдалося створити транзакцію з даних Dapp. Код: %s Ми зіткнулися з невідомою помилкою. Код помилки: %d. Якщо проблема не зникає — зверніться до нашої служби підтримки. + Багатоетапна транзакція + Для успішної обробки вашу транзакцію буде розділено на кілька частин. Потрібно буде кілька разів прикласти картку, щоб завершити її. Немає відкритих сеансів WalletConnect Упс. Немає сеансів. Не вдалося створити пару WalletConnect: %1$s @@ -1354,10 +1726,36 @@ Цю картку не можна використовувати з WalletConnect. Ця мережа не підтримується. Будь ласка, виберіть іншу мережу. Оберіть мережу + Транзакція обробляється. Будь ласка, прикладіть картку кілька разів, щоб завершити її. + Транзакція обробляється Сеанси WalletConnect Підключення до dApps WalletConnect Підключення може зайняти кілька секунд + Створіть гаманець Tangem + Від %s + Купіть Tangem Wallet — фізичний пристрій, який надійно зберігає ваш приватний ключ офлайн. + Апаратний гаманець + Миттєво створіть безпечний гаманець на телефоні + Мобільний гаманець + Що вибрати + Вже користуєтеся Tangem Wallet? + Відсканувати + Виберіть спосіб створення гаманця + Хочете купити гаманець Tangem? + Купити зараз + Відновіть існуючий гаманець за допомогою резервної копії Google Drive + Імпорт з Google Drive + Додати існуючий гаманець + Фізичні пристрої, які надійно зберігають ваш приватний ключ офлайн. + Відскануйте гаманець Tangem + Імпортуйте існуючий гаманець за допомогою сід-фрази. + Імпортувати гаманець + Введіть фразу відновлення + Ви успішно імпортували свій гаманець. + Імпортувати гаманець + Імпорт завершено + Імпортувати гаманець Ринкова ціна %s за 24 години Мережа %s @@ -1366,8 +1764,13 @@ Отримати з 10% знижкою Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗ’єднайте до трьох карток для бекапу. Відкрийте Tangem Wallet - Отримуйте сповіщення про вхідні транзакції в гаманці та оновлення Tangem. + Цей код доступу захищає ваш гаманець і використовується для входу та підписання транзакцій. + Встановити/Змінити код доступу + Змінити код доступу + Отримуйте сповіщення про вхідні транзакції в гаманці. + Push-повідомлення наразі можуть не працювати на пристроях Huawei. Ми активно працюємо над розв\'язанням цієї проблеми та плануємо випустити виправлення в наступному оновленні. Дякуємо за розуміння! Сповіщення про транзакції + Встановити код доступу Налаштування гаманця Tangem Використовуйте %s або відскануйте картку/кільце, щоб розблокувати доступ до вашого гаманця @@ -1443,6 +1846,7 @@ Ваш відгук мотивує нас робити гаманець Tangem Wallet ще кращим Подобається Tangem? Вам необхідно провести асоціацію токена, щоб мати можливість приймати його + Ви повинні відкрити трастлайн для свого токена, щоб мати можливість отримати на нього кошти Необхідна плата за оренду мережі Потрібна дія Чи зверталися в службу підтримки через додаток протягом 7 днів після створення гаманця? Якщо зверталися або не впевнені, натисніть «Так» і дотримуйтесь інструкцій. @@ -1468,6 +1872,7 @@ Відкрийте Trustline Потрібна мережа %s не додана до вашого портфеля. Спочатку додайте її, а потім виконайте підключення. Додати мережу до портфеля + Зловмисний домен Невідомий домен Все одно підключитися Помилка тайм-ауту. Будь ласка, спробуйте пізніше. @@ -1479,9 +1884,14 @@ Час очікування запиту вичерпано Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. Сеанс Wallet Connect було завершено + Підписати все одно Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. + Якщо проблема не зникає — зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою Tangem Wallet наразі не підтримує %s + Непідтримуваний dApp + Код помилки: 8 005. Якщо проблема не зникає - зверніться до нашої служби підтримки. + Ми помітили невідому помилку Ця мережа %s не підтримується Tangem Wallet і не може бути підключена. Непідтримувана мережа Tangem наразі не підтримує необхідну мережу для %s. @@ -1491,11 +1901,14 @@ Обрана не вірна картка або кільце Схоже, виникла проблема Усі dApps відключені + Дозвіл на використання Підтверджуючи, ви дозволяєте dApp або смартконтракту використовувати токени в майбутніх транзакціях. Адреса Підключення + Завантаження Мережа Мережі + Необмежено Гаманець Підключений додаток Підключені мережі @@ -1509,6 +1922,7 @@ З\'єднання Вміст Копіювати дані + Користувацький ліміт dApp відключено Розʼєднати все Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. @@ -1520,9 +1934,12 @@ Будь ласка, згенеруйте новий URI та спробуйте ще раз Термін для з’єднання минув Прогнозовані зміни + Не вдалося виконати симуляцію транзакції. Будь ласка, дійте з обережністю. + Оцінка не підтримується для %s Пропоновано %s Поповніть баланс, щоб покрити комісію мережі Недостатньо %1$s + Зловмисна транзакція Додайте %s мережі до вашого портфелю для цього гаманця В гаманці не додані необхідні мережі Нове з\'єднання @@ -1541,7 +1958,25 @@ До Запит транзакції Запит транзакції + Необмежена кількість + Переконайтеся, що при кожній спробі з\'єднання використовується новий та унікальний URI + URI вже використовується WalletConnect + Підозріла транзакція + Вже маєте гаманець Tangem? + Тисячі активів + Революційний апаратний гаманець + Швидка доставка + Почніть в один клік + Просто та надійно + Простий у використанні + Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище. + Створіть або імпортуйте програмний гаманець + Створіть або імпортуйте програмний гаманець на своєму телефоні. + Почніть з Мобільним гаманцем + Інші методи + Використовуйте апаратний гаманець Tangem + Дізнатися більше та придбати Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити @@ -1592,7 +2027,10 @@ Політика комісій Tangem комісію за обслуговування у розмірі 15% від отриманого доходу. Ваші кошти будуть автоматично переведені у Aave, як тільки мережеві комісії знизяться або ваш баланс досягне мінімально необхідної суми. + Комісії наразі вищі чим зазвичай із-за високої активності на ринку. Ви можете продовжити зараз чи повернутися пізніше, коли комісія знизиться. + Висока мережева комісія Історична прибутковість + Отримуй до %1$s APY на свій баланс Дозвіл для вашого токена в режимі Yield було відкликано. Відкрийте токен, щоб надати дозвіл знову. Потрібний дозвіл для токену Перевірте підключення до мережі @@ -1607,7 +2045,9 @@ Децентралізований та некастодіальний Використовуючи сервіс, ви погоджуєтесь з умовами провайдера %1$s та %2$s Підключити Aave + Підключіть %1$s %% APY\nна ваш баланс Aave %1$s%% • Плаваюча ставка + Динамічна процентна ставка Аave Серед. %s Дохідність за минулий рік @@ -1626,6 +2066,7 @@ Вимкнення цієї опції виведе ваші активи з Aave, отримаєте їх назад у %s у вашому гаманці і зупинить накопичення нагород. Комісію мережі стягує блокчейн при виході з режиму доходності. Завершити заробіток + Обіг Річна прибутковість (APY) APY Відсотки нараховуються автоматично. @@ -1633,15 +2074,20 @@ Режим дохідності Відправка ваших коштів Режим прибутковості + Встановлення контракту режиму дохідності Режим прибутковості увімкнено %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-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index a79d66c1d7..393b0cadb9 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -388,13 +388,14 @@ 通常需要最多15分钟 設置您的 Tangem 卡 正在发行您的卡片 - 正在为您准备卡片,这可能需要一些时间 + 卡片通常會在 5 分鐘內自動發行。如需人工審核的特殊情況,可能需要最長 48 小時。 Tangem Pay 我们无法验证您的资料。如有任何疑问,请联系客服。 很抱歉,我们无法验证您的身份 KYC进行中 查看状态 Tangem Pay 的 KYC 正在進行中 + 文件通常會在 5 分鐘內自動驗證完成。如需人工審核的特殊情況,可能需要最長 48 小時。 獲取您的免費 Tangem Visa 虛擬卡 使用 USDC 進行日常支付 获取卡片 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 582d2ca04c..59177b2da4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -323,6 +323,7 @@ No No address Not Added + Not available Not now Now OK @@ -437,6 +438,8 @@ This mechanism protects against proximity attacks on a card or ring. It will enforce a delay between reception and execution of a command. Passcode Before executing any command entailing a change of the card state, you will have to enter the passcode. + You can have either one mobile wallet or hardware wallet at the same time. Upgrade you wallet or delete it from the app to start using a Tangem cold wallet. + Upgrade to hardware wallet NFT Referral program Flip your device screen down to quickly hide and show balances @@ -539,6 +542,10 @@ ID: %s Transaction ID copied Swap any asset in your portfolio for this token + Higher speed means faster confirmation\nand a higher network fee. %s + Choose speed + Choose which token to use to pay\nthe network fee. %s + Choose token Market & News Tangem AI Trending Now @@ -555,6 +562,8 @@ Tangem feedback Can\'t send a transaction Coin description error + Not enough funds + Gasless transaction fee An error occurred An error occurred. Code: %s. Requires memo @@ -596,7 +605,7 @@ Create new wallet Level up your security with the superior Tangem hardware wallet. Hardware wallet - Move your current wallet into Tangem. + Convert your current mobile wallet into a Tangem cold wallet. Upgrade current wallet Go to backup Please back up your wallet before creating an access code. @@ -621,7 +630,7 @@ Are you sure you want to do this? Forget wallet Go to backup - View backup + View recovery phrase Forget wallet Forget anyway This wallet has a backup. Ensure you can recover it before forgetting the wallet. @@ -817,7 +826,7 @@ Quick recap Related News Related tokens - Sources + Related news Stay in the loop NFC is not available on your device About NFT @@ -985,6 +994,7 @@ Access code restore Identical cards Access code + You can have either a mobile wallet or a hardware wallet at one time. Upgrade your wallet or delete it from the app to start using a Tangem cold wallet. All offers Available with %s Providers facilitate transactions @@ -1239,6 +1249,11 @@ Prepare to scan card or ring you want to set up. Forget wallet This will remove the wallet from the application. The wallet itself can be added again. + Simple to use + Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. + No seed phrase + Best in class hardware wallet + Tangem Cold Wallet Name Put your token to work A network fee is a small payment required to process and confirm your transaction on the blockchain. @@ -1260,6 +1275,7 @@ APR APY Rewards automatically accumulate in your staking balance daily. + Rewards are compounded to your staking balance. Funds earned: %s Available Average Reward Rate How Staking Works? @@ -1506,16 +1522,21 @@ It usually takes up to 15 minutes Setting up your Tangem Card Issuing your card - We’re getting your card ready. This may take a little time. + The card is usually issued automatically within 5 minutes. In rare cases, if manual review is required, it may take up to 48 hours. 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 has failed KYC in progress View Status KYC in progress for Tangem Pay - Use the buttons below to view your current KYC status or cancel it. + Documents are usually verified automatically within 5 minutes. In rare cases, if manual review is required, it may take up to 48 hours. + KYC rejected + Hide KYC block + Sorry, we couldn\'t verify + your profile. Get your free Tangem Visa virtual card Use USDC for everyday payments Get card @@ -1610,6 +1631,7 @@ We\'ve encountered an error. Error code: %s. Please contact our support. Use %s or scan a card/ring to have access to your wallet Connection failed: This dApp uses Wallet Connect version 1.0, which is not supported. Please ensure the dApp supports Wallet Connect version 2.0 to connect successfully. + Upgrade to a hardware wallet Stay up to date with the latest features and news Real-time alerts for transactions, exchanges, and critical updates. Transaction Alerts @@ -1764,7 +1786,7 @@ Import an existing wallet with your recovery phrase. Import wallet Enter recovery phrase - You successfully backed up your wallet. + You successfully import up your wallet. Import wallet Import completed Import wallet @@ -1779,7 +1801,7 @@ This access code protects your wallet and is used to log in and sign transactions. Set/Change access code Change access code - Stay notified on wallet incoming transactions and Tangem updates. + Get push notifications for incoming wallet transactions. 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 Set access code 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 a788639680..536aa2afde 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 @@ -22,6 +22,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -110,7 +111,7 @@ internal class TangemPayRequestPerformer @Inject constructor( if (accessExpiresAt.isAfter(now)) { tokens.right() } else if (accessExpiresAt.isBefore(now) && refreshExpiresAt.isAfter(now)) { - refreshAuthTokens(userWalletId = userWalletId, refreshToken = tokens.refreshToken) + refreshAuthTokens(userWalletId = userWalletId, authTokens = tokens) } else { VisaApiError.RefreshTokenExpired.left() } @@ -119,13 +120,15 @@ internal class TangemPayRequestPerformer @Inject constructor( private suspend fun refreshAuthTokens( userWalletId: UserWalletId, - refreshToken: String, + authTokens: TangemPayAuthTokens, ): Either { val customerWalletAddress = getCustomerWalletAddress(userWalletId) + val idempotencyKey = requireNotNull(authTokens.idempotencyKey) { "Idempotency key is null" } val apiResponse = tangemPayAuthApi.refreshCustomerWalletAccessToken( + idempotencyKey = idempotencyKey, request = RefreshCustomerWalletAccessTokenRequest( authType = "customer_wallet", - refreshToken = refreshToken, + refreshToken = authTokens.refreshToken, ), ) val responseEither = when (apiResponse) { @@ -138,6 +141,7 @@ internal class TangemPayRequestPerformer @Inject constructor( expiresAt = response.expiresAt, refreshToken = response.refreshToken, refreshExpiresAt = response.refreshExpiresAt, + idempotencyKey = UUID.randomUUID().toString(), ) }.mapLeft { error -> Timber.tag(TAG).e("Can not refresh auth tokens: $error") 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 63d2853352..69fac79358 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 @@ -17,6 +17,7 @@ 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 java.util.UUID import javax.inject.Inject internal class DefaultTangemPayRemoteDataSource @Inject constructor( @@ -66,6 +67,7 @@ internal class DefaultTangemPayRemoteDataSource @Inject constructor( expiresAt = response.expiresAt, refreshToken = response.refreshToken, refreshExpiresAt = response.refreshExpiresAt, + idempotencyKey = UUID.randomUUID().toString(), ) } } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt index eb704a588a..b50008cf29 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt @@ -11,6 +11,7 @@ data class TangemPayAuthTokens( @Json(name = "expires_at") val expiresAt: Long, @Json(name = "refresh_token") val refreshToken: String, @Json(name = "refresh_expires_at") val refreshExpiresAt: Long, + @Json(name = "idempotency_key") val idempotencyKey: String? = null, ) fun TangemPayAuthTokens.getAuthHeader(): String { From 28944ba2879c22e21bf729baddc36ecdd25ac43f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Jan 2026 12:48:27 +0000 Subject: [PATCH 142/154] 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 e31c24f2d0..9f09031069 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-1374" +tangemBlockchainSdk = "releases-5.32-1335" #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-541" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 081128f2cf4e9f5013e545aa28b080df2dd3d6de Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Jan 2026 17:12:52 +0300 Subject: [PATCH 143/154] Updated on 2026-08-14 --- .../handlers/appsflyer/AppsFlyerAnalyticsHandler.kt | 3 ++- .../core/analytics/models/AppsFlyerOnlyEvent.kt | 3 ++- .../java/com/tangem/core/analytics/models/Basic.kt | 2 +- .../models/event/OnboardingAnalyticsEvent.kt | 12 ++---------- .../domain/onramp/analytics/OnrampAnalyticsEvent.kt | 5 +++-- .../staking/analytics/StakingAnalyticsEvent.kt | 7 ++++--- .../tangem/domain/walletconnect/WcAnalyticEvents.kt | 8 +++++--- .../createmobilewallet/CreateMobileWalletModel.kt | 1 - .../v2/common/analytics/OnboardingEvent.kt | 8 +------- .../v2/api/analytics/CommonSendAnalyticEvents.kt | 7 ++++--- .../send/v2/send/analytics/SendAnalyticEvents.kt | 3 ++- .../analytics/SendWithSwapAnalyticEvents.kt | 3 ++- .../com/tangem/feature/swap/analytics/SwapEvents.kt | 5 +++-- .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 9 ++------- .../analytics/utils/TokenListAnalyticsSender.kt | 1 - .../supply/api/analytics/YieldSupplyAnalytics.kt | 13 +++++++------ 16 files changed, 40 insertions(+), 50 deletions(-) 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 14a35c89cd..26675e0fef 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 @@ -26,8 +26,9 @@ class AppsFlyerAnalyticsHandler( } is AppsFlyerIncludedEvent -> { Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") + val replacedEvent = event.appsFlyerReplacedEvent ?: event.event client.logEvent( - event = AnalyticsEvent(category = event.category, event = event.appsFlyerReplacedEvent).id, + event = AnalyticsEvent(category = event.category, event = replacedEvent).id, params = event.params, ) } 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 index a265fcfd9f..757f31ff55 100644 --- 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 @@ -11,5 +11,6 @@ interface AppsFlyerOnlyEvent * Events implementing this interface will be sent to AppsFlyer along with other analytics handlers */ interface AppsFlyerIncludedEvent { - val appsFlyerReplacedEvent: String + val appsFlyerReplacedEvent: String? + get() = null } \ No newline at end of file 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 c59513d2fa..7a67705605 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 @@ -93,7 +93,7 @@ sealed class Basic( } this["Memo"] = memoType.name }, - ) { + ), AppsFlyerIncludedEvent { enum class MemoType { Empty, Full, Null } 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 c1cad4c1a7..e449459d0e 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 @@ -3,7 +3,6 @@ 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, @@ -16,8 +15,6 @@ 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( @@ -68,12 +65,7 @@ sealed class OnboardingAnalyticsEvent( put("Seed Phrase Length", seedPhraseLength.toString()) } }, - ), AppsFlyerIncludedEvent { - override val appsFlyerReplacedEvent = when (creationType) { - WalletCreationType.NewSeed -> "wallet_created_successfully" - WalletCreationType.SeedImport -> "wallet_imported" - } - } + ), AppsFlyerIncludedEvent sealed class WalletCreationType(val value: String) { data object NewSeed : WalletCreationType(value = "New Seed") @@ -93,7 +85,7 @@ sealed class OnboardingAnalyticsEvent( params = mapOf( AnalyticsParam.SOURCE to source, ), - ) + ), AppsFlyerIncludedEvent class ButtonImportWallet : SeedPhrase("Button - Import Wallet") class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened") 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 d9d98eac03..5010202433 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 @@ -8,6 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RESIDENCE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.domain.onramp.model.OnrampSource sealed class OnrampAnalyticsEvent( @@ -28,7 +29,7 @@ sealed class OnrampAnalyticsEvent( SOURCE to source.analyticsName, TOKEN_PARAM to tokenSymbol, ), - ) + ), AppsFlyerIncludedEvent class SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened") @@ -133,7 +134,7 @@ sealed class OnrampAnalyticsEvent( "Currency Type" to currency, PAYMENT_METHOD to paymentMethod, ), - ) + ), AppsFlyerIncludedEvent class MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount") class MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount") 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 e2950b602d..37c678b5e6 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,6 +2,7 @@ package com.tangem.domain.staking.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.models.staking.action.StakingActionType @@ -21,7 +22,7 @@ sealed class StakingAnalyticsEvent( params = mapOf( "Validators Count" to validatorsCount.toString(), ), - ) + ), AppsFlyerIncludedEvent class WhatIsStaking : StakingAnalyticsEvent( event = "Link - What Is Staking", @@ -40,7 +41,7 @@ sealed class StakingAnalyticsEvent( "Validator" to validator, "Action" to action.asAnalyticName, ), - ) + ), AppsFlyerIncludedEvent data class StakeInProgressScreenOpened( val validator: String, @@ -51,7 +52,7 @@ sealed class StakingAnalyticsEvent( "Validator" to validator, "Action" to action.asAnalyticName, ), - ) + ), AppsFlyerIncludedEvent class RewardScreenOpened : StakingAnalyticsEvent( event = "Reward Screen Opened", 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 aba74660de..27ca88f239 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 @@ -4,6 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.CheckDAppResult.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.WcAnalyticEvents.DAppVerificationStatus import com.tangem.domain.walletconnect.model.WcPairRequest @@ -19,7 +20,8 @@ sealed class WcAnalyticEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = WC_CATEGORY_NAME, event = event, params = params) { - class ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") + class ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened"), AppsFlyerIncludedEvent + class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( event = "Session Initiated", params = mapOf( @@ -74,7 +76,7 @@ sealed class WcAnalyticEvents( AnalyticsParam.BLOCKCHAIN to sessionForApprove.network.joinToString(",") { it.name }, DOMAIN_VERIFICATION to securityStatus.toAnalyticVerificationStatus(), ), - ) + ), AppsFlyerIncludedEvent class DAppConnectionFailed( errorCode: String, @@ -85,7 +87,7 @@ sealed class WcAnalyticEvents( AnalyticsParam.ERROR_CODE to errorCode, AnalyticsParam.ERROR_DESCRIPTION to errorMessage, ), - ) + ), AppsFlyerIncludedEvent class SessionDisconnected(dAppMetaData: WcAppMetaData) : WcAnalyticEvents( event = "dApp Disconnected", 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 1d9409b60b..d268abddb8 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 @@ -61,7 +61,6 @@ internal class CreateMobileWalletModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() 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 101eb5ff04..b8ed2c5101 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 @@ -34,12 +34,7 @@ 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" - } - } + ), AppsFlyerIncludedEvent sealed class WalletCreationType(val value: String) { data object PrivateKey : WalletCreationType(value = "Private Key") @@ -92,7 +87,6 @@ sealed class OnboardingEvent( ) : OnboardingEvent("Onboarding / Twins", event, params) { class ScreenOpened : Twins("Twinning Screen Opened") - class SetupStarted : Twins("Twin Setup Started") class SetupFinished : Twins("Twin Setup Finished") } 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 82527c66bf..79d9472a6e 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 @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent /** * Send analytics @@ -25,7 +26,7 @@ sealed class CommonSendAnalyticEvents( params = mapOf( SOURCE to source.analyticsName, ), - ) + ), AppsFlyerIncludedEvent /** Amount screen opened */ data class AmountScreenOpened( @@ -37,7 +38,7 @@ sealed class CommonSendAnalyticEvents( params = mapOf( SOURCE to source.analyticsName, ), - ) + ), AppsFlyerIncludedEvent /** Fee screen opened */ data class FeeScreenOpened( @@ -74,7 +75,7 @@ sealed class CommonSendAnalyticEvents( ) } }, - ) + ), AppsFlyerIncludedEvent /** If transaction delays notification is present */ data class NoticeTransactionDelays( 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 d8ccef5d07..c732a34b77 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 @@ -8,6 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.core.ui.extensions.capitalize import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -41,7 +42,7 @@ internal sealed class SendAnalyticEvents( } put(ENS_ADDRESS, ensAddress) }, - ) + ), AppsFlyerIncludedEvent data class ConvertTokenButtonClicked( val token: String, 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 e7e6ac5e7d..8f48f4568b 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 @@ -8,6 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -31,7 +32,7 @@ internal sealed class SendWithSwapAnalyticEvents( SEND_BLOCKCHAIN to fromToken.network.name, RECEIVE_BLOCKCHAIN to toToken.network.name, ), - ) + ), AppsFlyerIncludedEvent data class NoticeCanNotSwapToken( val fromToken: CryptoCurrency, 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 92e8672bb2..fe69da29ce 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 @@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType @@ -21,7 +22,7 @@ sealed class SwapEvents( data class SwapScreenOpened(val token: String) : SwapEvents( event = "Swap Screen Opened", params = mapOf("Token" to token), - ) + ), AppsFlyerIncludedEvent class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") @@ -96,7 +97,7 @@ sealed class SwapEvents( "Receive Blockchain" to receiveBlockchain, "Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex", ), - ) + ), AppsFlyerIncludedEvent class ProviderClicked : SwapEvents("Provider Clicked") 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 033a18e216..1eb6944175 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,7 +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.AppsFlyerIncludedEvent import com.tangem.core.analytics.models.OneTimeAnalyticsEvent import com.tangem.domain.models.wallet.UserWalletId @@ -18,16 +18,11 @@ sealed class WalletScreenAnalyticsEvent { event = "Topped up", params = mapOf(AnalyticsParam.CURRENCY to walletType.value), ), - OneTimeAnalyticsEvent { + OneTimeAnalyticsEvent, AppsFlyerIncludedEvent { 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 c3de527e77..5cfa978a2a 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 @@ -208,7 +208,6 @@ internal class TokenListAnalyticsSender @Inject constructor( } analyticsEventHandler.send(Basic.WalletToppedUp(userWallet.walletId, walletType)) - analyticsEventHandler.send(Basic.AppsFlyerWalletFunded(userWallet.walletId)) } } 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 6d5f950820..02f6ba56aa 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 @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent sealed class YieldSupplyAnalytics( event: String, @@ -20,7 +21,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class StartEarningScreen( val token: String, @@ -31,7 +32,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class StopEarningScreen( val token: String, @@ -42,7 +43,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class ButtonStartEarning( val token: String, @@ -97,7 +98,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class FundsEarned( val token: String, @@ -108,7 +109,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class FundsWithdrawn( val token: String, @@ -119,7 +120,7 @@ sealed class YieldSupplyAnalytics( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, ), - ) + ), AppsFlyerIncludedEvent data class EarnedFundsInfo( val token: String, From 63b07cccc3d51464d5770ea01b7662c4b8dad549 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 Jan 2026 16:14:14 +0300 Subject: [PATCH 144/154] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 3 +- .../configs/feature_toggles_config.json | 4 + core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 34 ++- core/res/src/main/res/values-ru/strings.xml | 34 ++- core/res/src/main/res/values/strings.xml | 29 ++- .../core/ui/components/block/BlockItem.kt | 22 +- .../core/ui/components/block/model/BlockUM.kt | 2 +- .../main/res/drawable/ic_backup_repeat_24.xml | 9 + .../main/res/drawable/ic_double_star_24.xml | 7 + .../res/drawable/ic_mobile_security_2_24.xml | 5 + .../main/res/drawable/ic_seed_phrase_24.xml | 15 ++ .../main/res/drawable/ic_smartphone_24.xml | 9 + .../main/res/drawable/ic_tangem_card_24.xml | 5 + .../CreateWalletStartModel.kt | 2 +- .../entity/CreateWalletStartUM.kt | 2 +- .../ui/CreateWalletStartContent.kt | 34 +-- .../preview/PreviewDetailsComponent.kt | 6 + .../preview/PreviewUserWalletListComponent.kt | 1 + .../features/details/entity/DetailsItemUM.kt | 6 + .../details/entity/UserWalletListUM.kt | 2 + .../features/details/model/DetailsModel.kt | 1 + .../details/model/UserWalletListModel.kt | 19 +- .../features/details/ui/DetailsScreen.kt | 29 ++- .../details/ui/UserWalletListBlock.kt | 40 ++-- .../features/details/utils/ItemsBuilder.kt | 15 +- features/hot-wallet/api/build.gradle.kts | 1 + .../hotwallet/HotWalletFeatureToggles.kt | 1 + .../hotwallet/WalletBackupComponent.kt | 1 + .../DefaultHotWalletFeatureToggles.kt | 3 + .../hotwallet/common/ui/OptionBlock.kt | 6 +- .../ui/CreateHardwareWalletContent.kt | 18 +- .../ui/CreateMobileWalletContent.kt | 11 +- .../upgradewallet/ui/UpgradeWalletContent.kt | 6 +- .../viewphrase/model/ViewPhraseModel.kt | 27 +-- .../walletbackup/entity/WalletBackupUM.kt | 1 - .../walletbackup/model/WalletBackupModel.kt | 22 +- .../walletbackup/ui/WalletBackupContent.kt | 195 ++++-------------- .../entity/WalletHardwareBackupUM.kt | 1 - .../model/WalletHardwareBackupModel.kt | 14 -- .../ui/WalletHardwareBackupContent.kt | 151 ++++++++++---- .../preview/PreviewWalletSettingsComponent.kt | 1 - .../entity/WalletSettingsItemUM.kt | 8 +- .../model/WalletSettingsModel.kt | 44 ++-- .../walletsettings/ui/WalletSettingsScreen.kt | 109 +++++----- .../walletsettings/utils/ItemsBuilder.kt | 22 -- .../utils/WalletCardItemDelegate.kt | 17 +- .../intents/WalletWarningsClickIntents.kt | 11 +- .../utils/WalletWarningsSingleEventSender.kt | 9 +- features/welcome/impl/build.gradle.kts | 1 + .../welcome/impl/model/WelcomeModel.kt | 58 +++++- gradle/tangem_dependencies.toml | 2 +- 53 files changed, 614 insertions(+), 464 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_backup_repeat_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_double_star_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_mobile_security_2_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_seed_phrase_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_smartphone_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_tangem_card_24.xml 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 19f07cabf5..d7f3a0cc33 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 @@ -190,6 +190,7 @@ internal class ChildFactory @Inject constructor( context = context, params = WalletBackupComponent.Params( userWalletId = route.userWalletId, + isColdWalletOptionShown = route.isColdWalletOptionShown, ), componentFactory = walletBackupComponentFactory, ) 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 e5bdf4dad1..7242c08646 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 @@ -231,7 +231,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class WalletBackup( val userWalletId: UserWalletId, - ) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}") + val isColdWalletOptionShown: Boolean, + ) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}/$isColdWalletOptionShown") @Serializable data class WalletHardwareBackup( 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 1cb7bb7d24..94775b09a4 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 @@ -35,6 +35,10 @@ "name": "HOT_WALLET_ENABLED", "version": "5.32.0" }, + { + "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", + "version": "5.32.0" + }, { "name": "TANGEM_PAY_ENABLED", "version": "5.31.0" diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index db29c44fbe..0a5f0530a8 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1767,7 +1767,7 @@ Importiere eine bestehende Wallet mit Deiner Wiederherstellungsphrase. Wallet importieren Wiederherstellungsphrase eingeben - Du hast Deine Wallet erfolgreich gesichert. + Du hast Deine Wallet erfolgreich importiert. Wallet importieren Import abgeschlossen Wallet importieren diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 785a92204c..565f5cf8f8 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -322,6 +322,7 @@ わかりました ブラウザで開く + または プライマリーカード プライマリーリング パスフレーズ @@ -430,6 +431,7 @@ このメカニズムは、カードまたはリングに対する近接攻撃から保護します。コマンドの受信と実行の間に遅延を強制します。 パスコード カードの状態の変更を伴うコマンドを実行する前に、パスコードを入力する必要があります。 + ハードウェアウォレットにアップグレード NFT 紹介プログラム デバイスの画面を下に向けると、残高をすばやく非表示にしたり表示したりできます。 @@ -443,6 +445,7 @@ 署名済み フィードバックを送信 詳細 + モバイルウォレットは同時に1つのみ利用できます。Tangemコールドウォレットにアップグレードすることも、新しいコールドウォレットと併用することも可能です。 インターネット接続を確認するか、別のネットワークに切り替えてください。 利用規約 デフォルトアドレス @@ -552,7 +555,8 @@ Tangemへのフィードバック 取引を送信できません コインの説明エラー - ガス代不要の取引手数料 + 残高不足 + 取引手数料 エラーが発生しました エラーが発生しました。コード: %s 。 メモが必要 @@ -564,10 +568,10 @@ 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください 許可を与える 無制限 - バックアップは2〜3枚のTangemカードを使用して作成されます。紛失や破損に備え、カードはそれぞれ安全な別の場所に保管してください。シードフレーズは不要です。 - 複数のカードでバックアップ + ハードウェアウォレット内に新しいアドレスが自動生成され、安全にすぐ利用できる状態になります。 + 新しいアドレス Tangemウォレットを追加 - あなたの秘密鍵はTangemカード内部で直接生成され、決してカードの外に出ることはありません。 + 秘密鍵はTangemカード内部で直接生成され、外部に出ることは一切ありません。 鍵生成 すべての暗号処理は、複製や物理的な改ざんに対する認証を受けたセキュアチップ内で行われます。 ハードウェアレベルのセキュリティ @@ -602,15 +606,20 @@ まずバックアップを完了する 未完了 その他の方法 + リカバリーフレーズは安全な場所に保管し、他人に知られないようにしてください。あわせて、資産をさらに強力に保護するためにアクセスコードを設定してください。 資金を保護するため、リカバリーフレーズは安全な場所に保管し、他人に知られないようにしてください。 リカバリーフレーズ アクセスコードでウォレットを保護するには、バックアップの手続きを完了してください。 ハードウェアウォレットにアップグレードするには、バックアップの手続きを完了してください。 + 業界最高レベルのセキュリティを実現するため、モバイルウォレットをTangemハードウェアウォレットにアップグレードしましょう。既存のウォレットをインポートするか、新しいウォレットへ資産を移行できます。 + コールドウォレットにアップグレード 秘密鍵は安全に暗号化され、スマートフォン上に保存されています 秘密鍵はデバイス上に保持されます リカバリーフレーズを使ってウォレットを作成または復元してください。 シードフレーズのバックアップ モバイルウォレットを作成する + モバイルウォレットは、いつでも安全にTangemカードまたはリングへ移行できます。 + ハードウェアウォレットにアップグレード 既存のウォレットをインポートする このリカバリーフレーズはすでにインポートされています。 モバイルウォレット @@ -628,14 +637,14 @@ ウォレットを削除する前にバックアップを行っていない場合、ウォレットへのアクセスを失うことを理解しています。 ウォレットを削除しても中身自体が消えるわけではなく、このデバイスから表示が消えるだけであることを理解しています。 アップグレード - シードフレーズは不要です。Tangemカードまたはリングが安全なバックアップとなります。 - Tangemでバックアップ + モバイルウォレットをTangemカードまたはリングにアップグレードしても、すべてのアドレスと残高は引き続き完全に利用できます。 + 同じアドレス アップグレードできません。このデバイスにはすでにウォレットが存在します。 別のデバイスを選択してください。このデバイスはアップグレードに使用できません。 操作中にエラーが発生しました。 処理中も、資金は安全に保たれ、常にアクセス可能です。 資金へのアクセス - ウォレット情報はアプリから削除され、ハードウェアウォレットに保存されます + アップグレード後、モバイルウォレットはアプリから削除され、ハードウェアウォレットに保存されます。リカバリーフレーズは引き続きご自身で管理してください。 セキュリティ全般 秘密鍵はアプリからTangemハードウェアウォレットへ移行されます キーの移行 @@ -1225,6 +1234,11 @@ 設定したいカードまたはリングをスキャンするために準備してください。 ウォレット削除 これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 + シンプルな操作 + 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 + シードフレーズ不要 + 業界最高水準のハードウェアウォレット + Tangemコールドウォレット 名前 トークンを活用しよう ネットワーク手数料とは、ブロックチェーン上で取引を処理し、承認するために支払う少額の料金です。 @@ -1391,6 +1405,7 @@ 手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。 とにかく便利 プロバイダー経由のスワップ + 資産 この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 @@ -1405,6 +1420,7 @@ 残高不足 許可を与える スワップ + スワップ中… 受け取る トークンを選択 利用不可 @@ -1531,6 +1547,8 @@ Tangem Payは現在一時的に利用できません。 Tangem Pay 下のボタンをクリックしてアクセスを復元してください + 現在引き出せるのは、保留中の取引分を差し引いたオンチェーンアドレス上のUSDC(Polygon)のみです。以前に返金や購入のキャンセルを行った場合、暗号資産アカウントの残高は2営業日以内に更新されます。 + ご注意ください PINコード これは私のウォレットです 残高非表示 @@ -1602,6 +1620,7 @@ エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。 %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 接続に失敗しました:このdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。 + ハードウェアウォレットにアップグレード 最新の機能とニュースをお届けします 取引・スワップ・重要な更新に関するリアルタイム通知。 取引アラート @@ -1925,6 +1944,7 @@ 迅速な配送 ワンタップで開始 シームレスで安全 + シードフレーズ不要 シンプルな操作 Tangemでハードウェアウォレットを作成しよう。キャッシュカードのようにスリムで、金庫のように安全。 ソフトウェアウォレットを作成またはインポート diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 20c4204701..ef16028c2d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -325,7 +325,7 @@ %d сеть %d сети %d сетей - %d сети + %d сетях Новый адрес Новости @@ -334,10 +334,12 @@ Нет Нет адреса Не добавлено + Не доступно Не сейчас Сейчас OK Открыть в браузере + или Основная карта Основное кольцо Парольная фраза @@ -450,6 +452,7 @@ Этот механизм защищает карту или кольцо от бесконтактных атак. Между сканированием и выполнением команды будет добавлена задержка. Пароль Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. + Апгрейд до аппаратного кошелька NFT Реферальная программа Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы @@ -463,6 +466,7 @@ Подписано Отправить отзыв Подробности + Одновременно у вас может быть только один мобильный кошелек. Его можно апгрейдить до Tangem cold wallet или использовать вместе с новым холодным кошельком. Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования Основной адрес @@ -569,6 +573,8 @@ Обращение в поддержку Tangem Не могу отправить транзакцию Ошибка в описании монеты + Недостаточно средств + Комиссия за транзакцию Произошла ошибка Произошла ошибка. Код: %s. Требуется memo @@ -580,10 +586,10 @@ Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно - Резервная копия создаётся на 2–3 картах Tangem. Храните их отдельно для безопасности — seed-фраза не нужна. - Бэкап на несколько карт + Новые адреса будут сгенерированы прямо в вашем аппаратном кошельке, готовы к использованию и полностью защищены. + Новые адреса Добавить кошелек Tangem - Ваш приватный ключ генерируется на карте Tangem и никогда не покидает её. + Ваш приватный ключ будет сгенерирован на карте Tangem и никогда не покинет её. Генерация ключа Операции с криптографией выполняются внутри защищённого чипа, устойчивого к клонированию и физическому взлому. Аппаратная безопасность @@ -618,15 +624,20 @@ Сначала завершите создание резервной копии Не завершено Другие способы + Сохраните фразу восстановления в надежном месте и держите её в секрете, чтобы защитить свои средства, а также установите код доступа для дополнительной безопасности. Сохраните фразу восстановления в безопасном месте и держите её в секрете. Фраза восстановления Чтобы защитить ваш кошелёк с помощью кода доступа, сначала завершите резервное копирование. Чтобы улучшить кошелёк до аппаратного, сначала создайте резервную копию. + Перенесите свой мобильный кошелек на аппаратный кошелек Tangem для максимальной безопасности. Импортируйте существующий кошелек или переведите средства на новый. + Апгрейд до холодного кошелька Ваши приватные ключи надёжно зашифрованы и хранятся на вашем телефоне Ключи хранятся в приложении Создайте или импортируйте кошелёк с помощью вашей фразы восстановления. Резервная копия Создать мобильный кошелек + Перенесите свой мобильный кошелек на Tangem-карту или кольцо в любое время. + Апгрейд до аппаратного кошелька Импортировать существующий Эта фраза восстановления уже была импортирована Мобильный кошелек @@ -644,14 +655,14 @@ Я понимаю, что если я не создал резервную копию кошелька перед его удалением, я могу потерять к нему доступ. Я понимаю, что удаление моего кошелька не стирает его — оно просто удаляет его с моего устройства. Апгрейд - Фраза восстановления больше не нужна — ваша карта или кольцо Tangem становятся вашей надёжной резервной копией. - Резервное копирование + Все ваши адреса и балансы остаются полностью доступными при апгрейде мобильного кошелька на Tangem-карту или кольцо. + Те же адреса Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек. Выберите другое устройство. Это нельзя использовать для обновления. Во время операции произошла ошибка. Ваши средства остаются в безопасности и полностью доступны в процессе. Доступ к средствам - Данные вашего кошелька будут удалены из приложения и сохранены на вашем устройстве Tangem. + После апгрейда ваш мобильный кошелек будет удалён из приложения и сохранён на аппаратном кошельке; фраза восстановления остаётся у вас. Общая безопасность Приватные ключи будут перемещены из приложения в вашу Tangem карту или кольцо Миграция ключей @@ -1277,6 +1288,11 @@ Подготовьтесь к сканированию кольца или карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. + Прост в использовании + Хранит вашу криптовалюту в безопасности и офлайн. Тонкий, как кредитная карта, надежнее банковского сейфа. + Без seed-фразы + Лучший в своём классе аппаратный кошелек + Холодный кошелек Tangem Имя Заставьте ваш токен работать Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне. @@ -1442,6 +1458,7 @@ Интуитивный обмен в пару касаний — без сложностей и ожидания Проще простого Обмен через провайдера + Ваши средства В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя. В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %1$s от биржи обратно на адрес пользователя \n\nПроскальзывание провайдера составляет до %2$s В сумму включена комиссия провайдера сервиса. @@ -1456,6 +1473,7 @@ Недостаточно средств Дать разрешение Обменять + Обмен… Вы получите Выберите токен не доступен @@ -1646,6 +1664,7 @@ Произошла ошибка. Код ошибки: %s. Попробуйте, пожалуйста, снова. Если проблема будет продолжать возникать — обратитесь в службу поддержки. Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения. + Апгрейд до аппаратного кошелька Будьте в курсе новых функций и новостей Мгновенные уведомления о транзакциях, обменах и важных обновлениях. Уведомления о транзакциях @@ -1957,6 +1976,7 @@ Быстрая доставка Начните в один клик Просто и надёжно + Без seed-фразы Интуитивно понятный Создайте аппаратный кошелёк с Tangem. Тонкий, как банковская карта, безопасный, как банковский сейф. Создать или импортировать мобильный кошелёк diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 59177b2da4..f7732464c2 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -328,6 +328,7 @@ Now OK Open in Browser + or Primary card Primary ring Passphrase @@ -438,7 +439,6 @@ This mechanism protects against proximity attacks on a card or ring. It will enforce a delay between reception and execution of a command. Passcode Before executing any command entailing a change of the card state, you will have to enter the passcode. - You can have either one mobile wallet or hardware wallet at the same time. Upgrade you wallet or delete it from the app to start using a Tangem cold wallet. Upgrade to hardware wallet NFT Referral program @@ -453,6 +453,7 @@ Signed Send feedback Details + You can have only one mobile wallet at a time. It can be upgraded to a Tangem cold wallet or used alongside a new cold wallet. Check your internet connection or switch to a different network Terms of service Default Address @@ -563,7 +564,7 @@ Can\'t send a transaction Coin description error Not enough funds - Gasless transaction fee + Transaction fee An error occurred An error occurred. Code: %s. Requires memo @@ -575,10 +576,10 @@ To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited - 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 + New addresses will be generated for you inside your hardware wallet, ready for use and fully secure. + New Addresses Add Tangem Wallet - Your private key is generated directly inside the Tangem card and never leaves it. + Your private key will be generated directly inside the Tangem card and will never leave it. Key Generation All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering. Hardware-Level Security @@ -613,15 +614,20 @@ Finalize backup first Incomplete Other methods + Save your recovery phrase in a secure place and keep it private to protect your funds, and set up an access code for additional security. 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 process. To upgrade to a hardware wallet, complete the backup process. + Upgrade your mobile wallet to a Tangem hardware wallet for the highest security. Import your wallet or transfer funds to a new one. + Upgrade to cold wallet Your private keys are securely encrypted and stored on your phone Private keys stay on your device Create or restore your wallet with a recovery phrase. Seed phrase backup Create Mobile Wallet + Transfer your mobile wallet to a Tangem card or ring anytime, securely. + Upgrade to hardware wallet Import existing wallet This recovery phrase has already been imported Mobile Wallet @@ -639,14 +645,14 @@ 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 + All your addresses and balances stay fully accessible when upgrading your mobile wallet to a Tangem card or ring. + Same Addresses Can\'t upgrade. A wallet already exists on this device. 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 information will be erased from the app and stored on your hardware wallet + After upgrading, your mobile wallet will be removed from the app and stored on your hardware wallet; your recovery phrase stays with you. General security Private keys will be moved from the app to your Tangem hardware wallet Key migration @@ -1420,6 +1426,7 @@ Hassle-free and intuitive, allowing you to swap tokens in just a few taps Simply Convenient Swap via provider + Your assets The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. The amount includes:\n• service provider\'s fee\n• network fee for sending %1$s from the exchange back to the user\'s address. \n\nProvider slippage is up to %2$s The amount includes the service provider\'s fee. @@ -1434,6 +1441,7 @@ Insufficient funds Give Permission Swap + Swapping... You receive Choose token not available @@ -1560,6 +1568,8 @@ Tangem Pay is temporarily unreachable Tangem Pay Click the button below to restore access + You can only withdraw USDC (Polygon) currently available on your on‑chain address balance, taking into account pending transactions. If you previously made a refund or cancelled a purchase, your crypto account balance will update within 2 business days. + Please note Your PIN code This is my wallet Balances hidden @@ -1631,7 +1641,7 @@ We\'ve encountered an error. Error code: %s. Please contact our support. Use %s or scan a card/ring to have access to your wallet Connection failed: This dApp uses Wallet Connect version 1.0, which is not supported. Please ensure the dApp supports Wallet Connect version 2.0 to connect successfully. - Upgrade to a hardware wallet + Upgrade to hardware wallet Stay up to date with the latest features and news Real-time alerts for transactions, exchanges, and critical updates. Transaction Alerts @@ -2002,6 +2012,7 @@ Fast delivery Start in one tap Seamless and secure + No seed phrase Simple to use Create a hardware wallet with Tangem. Slim as a bank card, secure as a bank vault. Create or import a software wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt index 66cf54df35..be7f8f5b2c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt @@ -27,16 +27,18 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start), ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = model.iconRes), - tint = when (model.accentType) { - BlockUM.AccentType.NONE -> TangemTheme.colors.icon.secondary - BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent - BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning - }, - contentDescription = null, - ) + if (model.iconRes != null) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = model.iconRes), + tint = when (model.accentType) { + BlockUM.AccentType.NONE -> TangemTheme.colors.icon.secondary + BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent + BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning + }, + contentDescription = null, + ) + } Text( modifier = Modifier.weight(1f), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt index 4988f72eff..f0079ad959 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt @@ -7,7 +7,7 @@ import javax.annotation.concurrent.Immutable data class BlockUM( val text: TextReference, - @DrawableRes val iconRes: Int, + @DrawableRes val iconRes: Int?, val onClick: () -> Unit, val accentType: AccentType = AccentType.NONE, val endContent: EndContent = EndContent.None, diff --git a/core/ui/src/main/res/drawable/ic_backup_repeat_24.xml b/core/ui/src/main/res/drawable/ic_backup_repeat_24.xml new file mode 100644 index 0000000000..3579504b2c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_backup_repeat_24.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_double_star_24.xml b/core/ui/src/main/res/drawable/ic_double_star_24.xml new file mode 100644 index 0000000000..78ca302987 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_double_star_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_mobile_security_2_24.xml b/core/ui/src/main/res/drawable/ic_mobile_security_2_24.xml new file mode 100644 index 0000000000..20f8ba11fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_security_2_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_seed_phrase_24.xml b/core/ui/src/main/res/drawable/ic_seed_phrase_24.xml new file mode 100644 index 0000000000..7ec0b491f4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_seed_phrase_24.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_smartphone_24.xml b/core/ui/src/main/res/drawable/ic_smartphone_24.xml new file mode 100644 index 0000000000..eb4558d24b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_smartphone_24.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_card_24.xml b/core/ui/src/main/res/drawable/ic_tangem_card_24.xml new file mode 100644 index 0000000000..2f4eabe10a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_card_24.xml @@ -0,0 +1,5 @@ + + + + + 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 8c7bb0d2eb..2183fc05a0 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 @@ -92,7 +92,7 @@ internal class CreateWalletStartModel @Inject constructor( onPrimaryButtonClick = ::onBuyClick, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), - otherMethodDescription = resourceReference(R.string.welcome_create_wallet_mobile_description), + otherMethodDescription = null, otherMethodClick = ::onStartWithMobileWalletClick, onBackClick = { router.pop() }, onScanClick = ::onScanClick, 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 b45777a20a..bce2d230b2 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 @@ -12,7 +12,7 @@ internal data class CreateWalletStartUM( val shouldShowScanSecondaryButton: Boolean, val primaryButtonText: TextReference, val onPrimaryButtonClick: () -> Unit, - val otherMethodDescription: TextReference, + val otherMethodDescription: TextReference?, val otherMethodTitle: TextReference, val otherMethodClick: () -> Unit, val onScanClick: () -> Unit, 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 ecf741b0f0..e113e454a5 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 @@ -181,7 +181,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi .height(16.dp), ) Text( - text = stringResourceSafe(R.string.welcome_create_wallet_other_method), + text = stringResourceSafe(R.string.common_or), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -193,19 +193,21 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi .scale(scaleX = -1f, scaleY = 1f), ) } - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - top = 16.dp, - end = 16.dp, - ), - text = state.otherMethodDescription.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) + if (state.otherMethodDescription != null) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + ), + text = state.otherMethodDescription.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } Row( modifier = Modifier .wrapContentWidth() @@ -429,9 +431,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide onPrimaryButtonClick = { }, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), - otherMethodDescription = resourceReference( - R.string.welcome_create_wallet_mobile_description, - ), + otherMethodDescription = null, otherMethodClick = { }, onBackClick = { }, onScanClick = { }, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 0a9a273352..45c7cd73ca 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -12,6 +12,7 @@ import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.ui.DetailsScreen import com.tangem.features.details.utils.ItemsBuilder import com.tangem.features.details.utils.SocialsBuilder +import com.tangem.features.hotwallet.HotWalletFeatureToggles import kotlinx.coroutines.runBlocking internal class PreviewDetailsComponent : DetailsComponent { @@ -19,9 +20,14 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( router = DummyRouter(), + hotWalletFeatureToggles = object : HotWalletFeatureToggles { + override val isHotWalletEnabled: Boolean = true + override val isWalletCreationRestrictionEnabled: Boolean = true + }, ).buildAll( isWalletConnectAvailable = true, isSupportChatAvailable = true, + hasAnyMobileWallet = true, userWalletId = UserWalletId(""), onSupportEmailClick = {}, onSupportChatClick = {}, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 86ecb2a42f..e56710ead0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -50,6 +50,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { ), addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), isWalletSavingInProgress = true, + addNewWalletIconRes = R.drawable.ic_plus_24, onAddNewWalletClick = {}, ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt index 99c416a63a..5e7ebe0773 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @Immutable @@ -27,4 +28,9 @@ internal sealed class DetailsItemUM { data object UserWalletList : DetailsItemUM() { override val id: String = "user_wallet_list" } + + data class UnderSectionText( + override val id: String, + val text: TextReference, + ) : DetailsItemUM() } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..bc4baf7d42 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.details.entity +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference @@ -10,5 +11,6 @@ internal data class UserWalletListUM( val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, + @DrawableRes val addNewWalletIconRes: Int?, val onAddNewWalletClick: () -> Unit, ) \ No newline at end of file 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 1c4c53f372..9d316e6c90 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 @@ -97,6 +97,7 @@ internal class DetailsModel @Inject constructor( itemsBuilder.buildAll( isWalletConnectAvailable = isWalletConnectAvailable, isSupportChatAvailable = feedbackFeatureToggles.isUsedeskEnabled, + hasAnyMobileWallet = getWalletsUseCase.invokeSync().any { it is UserWallet.Hot }, userWalletId = params.userWalletId, onSupportEmailClick = ::sendFeedback, onSupportChatClick = ::openUseDesk, 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 badebd605e..0a0dd71c89 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 @@ -60,6 +60,7 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, + addNewWalletIconRes = R.drawable.ic_plus_24, ), ) @@ -81,10 +82,11 @@ internal class UserWalletListModel @Inject constructor( value.copy( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) { - resourceReference(R.string.user_wallet_list_add_button) - } else { - resourceReference(R.string.scan_card_settings_button) + addNewWalletText = when { + shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled -> { + resourceReference(R.string.user_wallet_list_add_button) + } + else -> resourceReference(R.string.scan_card_settings_button) }, ) } @@ -92,7 +94,14 @@ internal class UserWalletListModel @Inject constructor( private fun onAddNewWalletClick() { if (hotWalletFeatureToggles.isHotWalletEnabled) { analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.Settings)) - router.push(AppRoute.CreateWalletSelection) + + if (hotWalletFeatureToggles.isWalletCreationRestrictionEnabled) { + withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet(modelScope) + } + } else { + router.push(AppRoute.CreateWalletSelection) + } } else { withProgress(isWalletSavingInProgress) { userWalletSaver.scanAndSaveUserWallet(modelScope) 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 6be90f525b..0afe3711b2 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -18,10 +19,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag 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.SpacerH16 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.decompose.ComposableContentComponent +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 @@ -68,7 +73,7 @@ private fun Content( ) { LazyColumn( modifier = modifier.testTag(DetailsScreenTestTags.SCREEN_CONTAINER), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.SpaceBetween, contentPadding = PaddingValues( top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16, @@ -94,6 +99,7 @@ private fun Content( } item(key = "footer") { + SpacerH16() Footer( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), model = state.footer, @@ -108,6 +114,16 @@ private fun Block( userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { + if (model is DetailsItemUM.UnderSectionText) { + UnderSectionTextBlock( + modifier = Modifier.fillMaxWidth(), + text = model.text, + ) + return + } + + SpacerH16() + Column( modifier = modifier .fillMaxWidth() @@ -142,10 +158,21 @@ private fun Block( is DetailsItemUM.UserWalletList -> { userWalletListBlockContent.Content(modifier = itemModifier) } + is DetailsItemUM.UnderSectionText -> { /* Handled above */ } } } } +@Composable +private fun UnderSectionTextBlock(text: TextReference, modifier: Modifier = Modifier) { + Text( + modifier = modifier.padding(horizontal = 30.dp, vertical = 8.dp), + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + @Composable private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { Column( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index ec27900868..f06856c4df 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,6 +1,7 @@ package com.tangem.features.details.ui import android.content.res.Configuration +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator @@ -23,7 +24,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM -import com.tangem.features.details.impl.R @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { @@ -42,6 +42,7 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M text = state.addNewWalletText, isInProgress = state.isWalletSavingInProgress, onClick = state.onAddNewWalletClick, + icon = state.addNewWalletIconRes, ) } } @@ -49,6 +50,7 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M @Composable private fun AddWalletButton( text: TextReference, + @DrawableRes icon: Int?, isInProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -65,23 +67,25 @@ private fun AddWalletButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - AnimatedContent( - modifier = Modifier.size(TangemTheme.dimens.size24), - targetState = isInProgress, - label = "Add wallet progress", - ) { isInProgress -> - if (isInProgress) { - CircularProgressIndicator( - modifier = Modifier.size(TangemTheme.dimens.size24), - color = TangemTheme.colors.icon.accent, - ) - } else { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_plus_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) + if (icon != null) { + AnimatedContent( + modifier = Modifier.size(TangemTheme.dimens.size24), + targetState = isInProgress, + label = "Add wallet progress", + ) { isInProgress -> + if (isInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.accent, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = icon), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } } } 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 7ee2b6eccd..9746070c35 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.R +import com.tangem.features.hotwallet.HotWalletFeatureToggles import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -17,12 +18,16 @@ import javax.inject.Inject private const val TANGEM_PAY_ITEM_ID = "get_tangem_pay" @ModelScoped -internal class ItemsBuilder @Inject constructor(private val router: Router) { +internal class ItemsBuilder @Inject constructor( + private val router: Router, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, +) { @Suppress("LongParameterList") fun buildAll( isWalletConnectAvailable: Boolean, isSupportChatAvailable: Boolean, + hasAnyMobileWallet: Boolean, userWalletId: UserWalletId, onSupportEmailClick: () -> Unit, onSupportChatClick: () -> Unit, @@ -30,6 +35,14 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ): ImmutableList = buildList { buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add) buildUserWalletListBlock().let(::add) + + if (hotWalletFeatureToggles.isWalletCreationRestrictionEnabled && hasAnyMobileWallet) { + DetailsItemUM.UnderSectionText( + id = "only_one_mobile_wallet_explanation", + text = resourceReference(R.string.only_one_mobile_wallet_explanation), + ).let(::add) + } + buildShopBlock(onBuyClick).let(::add) buildSettingsBlock().let(::add) buildSupportBlock( diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts index a51cd40254..7e7bd837fd 100644 --- a/features/hot-wallet/api/build.gradle.kts +++ b/features/hot-wallet/api/build.gradle.kts @@ -9,6 +9,7 @@ android { } dependencies { + implementation(projects.common.routing) /* Project - Domain */ implementation(projects.domain.models) diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt index f8ecf58ffe..c0526863e1 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.hotwallet interface HotWalletFeatureToggles { val isHotWalletEnabled: Boolean + val isWalletCreationRestrictionEnabled: Boolean } \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletBackupComponent.kt index c5c270623f..0d4df73fb4 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletBackupComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletBackupComponent.kt @@ -8,6 +8,7 @@ interface WalletBackupComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, + val isColdWalletOptionShown: Boolean, ) interface Factory : ComponentFactory diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt index cadb09331f..b635ab78db 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -7,4 +7,7 @@ internal class DefaultHotWalletFeatureToggles( ) : HotWalletFeatureToggles { override val isHotWalletEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_ENABLED") + override val isWalletCreationRestrictionEnabled: Boolean + get() = isHotWalletEnabled && + featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_CREATION_RESTRICTION_ENABLED") } \ No newline at end of file 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 bcc2a2c440..168491ff06 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 @@ -26,11 +26,11 @@ private const val DISABLED_COLORS_ALPHA = 0.5f internal fun OptionBlock( title: String, description: String, - badge: (@Composable () -> Unit)?, onClick: (() -> Unit)?, - enabled: Boolean, - backgroundColor: Color, modifier: Modifier = Modifier, + backgroundColor: Color = TangemTheme.colors.background.primary, + badge: (@Composable () -> Unit)? = null, + enabled: Boolean = true, ) { Column( modifier = modifier 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 095790c30a..08afdafd30 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 @@ -66,7 +66,7 @@ internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier top = 20.dp, end = 16.dp, ), - text = stringResourceSafe(R.string.wallet_create_common_title), + text = stringResourceSafe(R.string.hardware_wallet_create_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -74,22 +74,22 @@ internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier FeatureBlock( modifier = Modifier .padding(top = 32.dp), - title = stringResourceSafe(R.string.hw_upgrade_key_migration_title), - description = stringResourceSafe(R.string.hw_upgrade_key_migration_description), - iconRes = R.drawable.ic_mobile_security_24, + title = stringResourceSafe(R.string.hardware_wallet_key_feature_title), + description = stringResourceSafe(R.string.hardware_wallet_key_feature_description), + iconRes = R.drawable.ic_mobile_security_2_24, ) FeatureBlock( modifier = Modifier .padding(top = 24.dp), - title = stringResourceSafe(R.string.hw_upgrade_funds_access_title), - description = stringResourceSafe(R.string.hw_upgrade_funds_access_description), - iconRes = R.drawable.ic_knight_shield_24, + title = stringResourceSafe(R.string.hardware_wallet_backup_feature_title), + description = stringResourceSafe(R.string.hardware_wallet_backup_feature_description), + iconRes = R.drawable.ic_double_star_24, ) FeatureBlock( modifier = Modifier .padding(top = 24.dp), - title = stringResourceSafe(R.string.hw_upgrade_general_security_title), - description = stringResourceSafe(R.string.hw_upgrade_general_security_description), + title = stringResourceSafe(R.string.hardware_wallet_security_feature_title), + description = stringResourceSafe(R.string.hardware_wallet_security_feature_description), iconRes = R.drawable.ic_protect_24, ) } 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 e6916bdcfd..bf0f02e9b8 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 @@ -75,14 +75,21 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo .padding(top = 32.dp), title = stringResourceSafe(R.string.hw_create_keys_title), description = stringResourceSafe(R.string.hw_create_keys_description), - iconRes = R.drawable.ic_lock_24, + iconRes = R.drawable.ic_protect_24, ) FeatureBlock( modifier = Modifier .padding(top = 24.dp), title = stringResourceSafe(R.string.hw_create_seed_title), description = stringResourceSafe(R.string.hw_create_seed_description), - iconRes = R.drawable.ic_settings_24, + iconRes = R.drawable.ic_seed_phrase_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_create_upgrade_title), + description = stringResourceSafe(R.string.hw_create_upgrade_description), + iconRes = R.drawable.ic_tangem_card_24, ) } SecondaryButton( 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 a643ee75e0..7dc3ca93cf 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 @@ -83,9 +83,9 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M FeatureBlock( modifier = Modifier .padding(top = 24.dp), - title = stringResourceSafe(R.string.hw_upgrade_funds_access_title), - description = stringResourceSafe(R.string.hw_upgrade_funds_access_description), - iconRes = R.drawable.ic_knight_shield_24, + title = stringResourceSafe(R.string.hw_upgrade_backup_title), + description = stringResourceSafe(R.string.hw_upgrade_backup_description), + iconRes = R.drawable.ic_smartphone_24, ) FeatureBlock( modifier = Modifier diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt index a0b991c0a4..808f1df0b6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -48,19 +49,19 @@ internal class ViewPhraseModel @Inject constructor( private fun loadSeedPhrase() { val userWallet = getUserWalletUseCase(params.userWalletId) .getOrElse { error("User wallet with id ${params.userWalletId} not found") } - if (userWallet is UserWallet.Hot) { - modelScope.launch { - val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) - .getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") } - .mnemonic - .mnemonicComponents - uiState.update { - it.copy( - words = words.mapIndexed { index, s -> - EnumeratedTwoColumnGridItem(index + 1, s) - }.toImmutableList(), - ) - } + as? UserWallet.Hot ?: return + + modelScope.launch { + val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) + .getOrElse { error -> Timber.e(error); throw error } + .mnemonic.mnemonicComponents + + uiState.update { + it.copy( + words = words.mapIndexed { index, s -> + EnumeratedTwoColumnGridItem(index + 1, s) + }.toImmutableList(), + ) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index 9afce42e0d..a1219ed46a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -8,7 +8,6 @@ internal data class WalletBackupUM( val recoveryPhraseOption: LabelUM?, val googleDriveOption: LabelUM?, val googleDriveStatus: BackupStatus, - val onBuyClick: () -> Unit, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, val onHardwareWalletClick: () -> Unit, 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 bdc95378ca..7c284f9cef 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 @@ -2,21 +2,17 @@ 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.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.navigation.url.UrlOpener import com.tangem.core.ui.R 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.models.wallet.UserWallet import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents -import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase import com.tangem.features.hotwallet.WalletBackupComponent @@ -36,8 +32,6 @@ internal class WalletBackupModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, - private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, - private val urlOpener: UrlOpener, private val router: Router, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, @@ -54,7 +48,7 @@ internal class WalletBackupModel @Inject constructor( hardwareWalletOption = LabelUM( text = resourceReference(R.string.common_recommended), style = LabelStyle.ACCENT, - ), + ).takeIf { params.isColdWalletOptionShown }, recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, @@ -64,7 +58,6 @@ internal class WalletBackupModel @Inject constructor( style = LabelStyle.REGULAR, ), googleDriveStatus = BackupStatus.ComingSoon, - onBuyClick = ::onBuyClick, onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, onHardwareWalletClick = ::onHardwareWalletClick, @@ -129,14 +122,6 @@ internal class WalletBackupModel @Inject constructor( backedUp = userWallet.backedUp, ) - private fun onBuyClick() { - analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup)) - modelScope.launch { - generateBuyTangemCardLinkUseCase - .invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).let { urlOpener.openUrl(it) } - } - } - private fun onRecoveryPhraseClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase()) if (uiState.value.backedUp) { @@ -157,10 +142,9 @@ internal class WalletBackupModel @Inject constructor( ) } else { router.push( - AppRoute.CreateWalletBackup( + AppRoute.WalletActivation( userWalletId = params.userWalletId, - analyticsSource = AnalyticsParam.ScreensSources.Backup.value, - analyticsAction = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup.value, + isBackupExists = false, ), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index d3e452bab2..dc2787f9c6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -1,36 +1,25 @@ package com.tangem.features.hotwallet.walletbackup.ui import android.content.res.Configuration -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image import androidx.compose.foundation.background 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.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign 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 com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton 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 import com.tangem.core.ui.components.rows.NetworkTitle -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.common.ui.OptionBlock @@ -61,33 +50,31 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod end = 16.dp, ), ) { - Banner(state) + if (state.hardwareWalletOption != null) { + OptionBlock( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + title = stringResourceSafe(R.string.hw_backup_upgrade_title), + description = stringResourceSafe(R.string.hw_backup_upgrade_description), + badge = { Label(state.hardwareWalletOption) }, + onClick = state.onHardwareWalletClick, + enabled = true, + backgroundColor = TangemTheme.colors.background.primary, + ) - OptionBlock( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - title = stringResourceSafe(R.string.hw_backup_hardware_title), - description = stringResourceSafe(R.string.hw_backup_hardware_description), - badge = { - state.hardwareWalletOption?.let { Label(it) } - }, - onClick = state.onHardwareWalletClick, - enabled = true, - backgroundColor = TangemTheme.colors.background.primary, - ) - NetworkTitle( - modifier = Modifier - .padding(top = 8.dp), - title = { - Text( - modifier = Modifier, - text = stringResourceSafe(R.string.onboarding_create_wallet_options_button_options), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) + NetworkTitle( + modifier = Modifier.padding(top = 8.dp), + title = { + Text( + modifier = Modifier, + text = stringResourceSafe(R.string.onboarding_create_wallet_options_button_options), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) + } OptionBlock( modifier = Modifier, title = stringResourceSafe(R.string.hw_backup_seed_title), @@ -116,120 +103,6 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod } } -@Suppress("LongMethod") -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun Banner(state: WalletBackupUM, modifier: Modifier = Modifier) { - ForceDarkTheme { - Column( - modifier = modifier - .background( - color = TangemTheme.colors.background.primary, - shape = RoundedCornerShape(16.dp), - ), - ) { - Column( - modifier = Modifier - .padding( - start = 12.dp, - top = 20.dp, - end = 12.dp, - ), - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = "Tangem Wallet", - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - top = 4.dp, - ), - text = "Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 24.dp, - top = 16.dp, - end = 24.dp, - ), - horizontalArrangement = Arrangement.Center, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - FeatureItem( - iconResId = R.drawable.ic_shield_check_16, - text = resourceReference(R.string.welcome_create_wallet_feature_class), - ) - FeatureItem( - iconResId = R.drawable.ic_flash_16, - text = resourceReference(R.string.welcome_create_wallet_feature_delivery), - ) - FeatureItem( - iconResId = R.drawable.ic_sparkles_16, - text = resourceReference(R.string.welcome_create_wallet_feature_use), - ) - } - - Box( - modifier = Modifier - .padding( - start = 8.dp, - top = 12.dp, - end = 8.dp, - bottom = 20.dp, - ), - ) { - Image( - painter = painterResource(id = R.drawable.img_tangem_cards_vertical), - contentDescription = null, - ) - SecondaryButton( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - text = stringResourceSafe(R.string.details_buy_wallet), - onClick = state.onBuyClick, - ) - } - } - } - } -} - -@Composable -private fun FeatureItem(@DrawableRes iconResId: Int, text: TextReference) { - Row( - modifier = Modifier - .wrapContentWidth() - .padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(iconResId), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - Text( - text = text.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 @@ -256,7 +129,6 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider, val onBackClick: () -> Unit, val onBuyClick: () -> Unit, 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 efcd39aa14..ae34bf84a6 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 @@ -32,10 +32,8 @@ import com.tangem.features.hotwallet.wallethardwarebackup.entity.WalletHardwareB import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -119,7 +117,6 @@ internal class WalletHardwareBackupModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened()) - showPurchaseBlockWithDelay() } override fun onDestroy() { @@ -127,13 +124,6 @@ internal class WalletHardwareBackupModel @Inject constructor( super.onDestroy() } - private fun showPurchaseBlockWithDelay() { - modelScope.launch { - delay(SHOW_PURCHASE_BLOCK_DELAY) - uiState.update { it.copy(showPurchaseBlock = true) } - } - } - private fun onCreateNewWalletClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet()) router.push(AppRoute.CreateHardwareWallet) @@ -175,8 +165,4 @@ internal class WalletHardwareBackupModel @Inject constructor( .invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).let { urlOpener.openUrl(it) } } } - - companion object { - private const val SHOW_PURCHASE_BLOCK_DELAY = 3000L - } } \ No newline at end of file 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 8e4eafbf78..41073ffc50 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 @@ -1,26 +1,26 @@ package com.tangem.features.hotwallet.wallethardwarebackup.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign 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.buttons.common.TangemButtonSize 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 -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.extensions.* +import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.common.ui.OptionBlock @@ -71,6 +71,8 @@ internal fun WalletHardwareBackupContent(state: WalletHardwareBackupUM, modifier end = 16.dp, ), ) { + Banner(onBuyClick = state.onBuyClick) + state.blocks.forEach { block -> OptionBlock( modifier = Modifier @@ -86,45 +88,119 @@ internal fun WalletHardwareBackupContent(state: WalletHardwareBackupUM, modifier ) } } - AnimatedVisibility(state.showPurchaseBlock) { - PurchaseBlock( - onBuyClick = state.onBuyClick, - ) + } +} + +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun Banner(onBuyClick: () -> Unit, modifier: Modifier = Modifier) { + ForceDarkTheme { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(16.dp), + ), + ) { + Column( + modifier = Modifier + .padding( + start = 12.dp, + top = 20.dp, + end = 12.dp, + ), + ) { + Text( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_tangem_wallet), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + top = 4.dp, + ), + text = stringResourceSafe(R.string.hw_backup_banner_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + top = 16.dp, + end = 24.dp, + ), + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + FeatureItem( + iconResId = R.drawable.ic_shield_check_16, + text = resourceReference(R.string.welcome_create_wallet_feature_class), + ) + FeatureItem( + iconResId = R.drawable.ic_flash_16, + text = resourceReference(R.string.welcome_create_wallet_feature_delivery), + ) + FeatureItem( + iconResId = R.drawable.ic_sparkles_16, + text = resourceReference(R.string.welcome_create_wallet_feature_seedphrase), + ) + } + + Box( + modifier = Modifier + .padding( + start = 8.dp, + top = 12.dp, + end = 8.dp, + bottom = 20.dp, + ), + ) { + Image( + painter = painterResource(id = R.drawable.img_tangem_cards_vertical), + contentDescription = null, + ) + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.details_buy_wallet), + onClick = onBuyClick, + ) + } + } } } } @Composable -private fun PurchaseBlock(onBuyClick: () -> Unit, modifier: Modifier = Modifier) { +private fun FeatureItem(@DrawableRes iconResId: Int, text: TextReference) { Row( - modifier = modifier - .fillMaxWidth() - .padding(16.dp) - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - horizontal = 20.dp, - vertical = 16.dp, - ), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - Text( - modifier = Modifier - .weight(1f) - .padding(end = 16.dp), - text = stringResourceSafe(R.string.wallet_add_hardware_purchase), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - maxLines = 3, - overflow = TextOverflow.Ellipsis, + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(iconResId), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, ) - - SecondaryButton( - text = stringResourceSafe(R.string.wallet_import_buy_title), - onClick = onBuyClick, - size = TangemButtonSize.RoundedAction, + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, ) } } @@ -156,7 +232,6 @@ private fun PreviewWalletHardwareBackupContent() { onClick = { }, ), ), - showPurchaseBlock = true, onBuyClick = { }, ), ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 34449ac010..4fa7a53f60 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -65,7 +65,6 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onNotificationsDescriptionClick = {}, isNotificationsPermissionGranted = false, onAccessCodeClick = {}, - onUpgradeWalletClick = {}, onBackupClick = {}, onCardSettingsClick = {}, accountsUM = previewAccounts(), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index a495db9417..d9ff609d19 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -31,6 +31,7 @@ internal sealed class WalletSettingsItemUM { val text: TextReference, val isEnabled: Boolean, val imageState: ImageState, + val additionalBlock: BlockUM? = null, val onClick: () -> Unit, ) : WalletSettingsItemUM() @@ -46,13 +47,6 @@ internal sealed class WalletSettingsItemUM { val title: TextReference, val description: TextReference, ) : WalletSettingsItemUM() - - data class UpgradeWallet( - override val id: String, - val title: TextReference, - val description: TextReference, - val onClick: () -> Unit, - ) : WalletSettingsItemUM() } @Immutable 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 8e97fb4927..f817314f14 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 @@ -96,7 +96,10 @@ internal class WalletSettingsModel @Inject constructor( val params: WalletSettingsComponent.Params = paramsContainer.require() val dialogNavigation = SlotNavigation() val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private val walletCardItemDelegate = walletCardItemDelegateFactory.create(dialogNavigation) + private val walletCardItemDelegate = walletCardItemDelegateFactory.create( + dialogNavigation = dialogNavigation, + onUpgradeHotWalletClick = ::onUpgradeWalletClick, + ) val state: MutableStateFlow = MutableStateFlow( value = WalletSettingsUM( @@ -249,7 +252,6 @@ internal class WalletSettingsModel @Inject constructor( onCheckedNotificationsChanged = ::onCheckedNotificationsChange, onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, onAccessCodeClick = { onAccessCodeClick(userWallet) }, - onUpgradeWalletClick = { onUpgradeWalletClick() }, onBackupClick = ::onBackupClick, onCardSettingsClick = ::onCardSettingsClick, accountsUM = accountList, @@ -374,10 +376,7 @@ internal class WalletSettingsModel @Inject constructor( val isCodeSet = userWallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonAccessCode(isCodeSet)) if (!state.value.isWalletBackedUp) { - showMakeBackupAtFirstAlertBS( - isUpgradeFlow = false, - action = WalletSettingsAnalyticEvents.NoticeBackupFirst.Action.AccessCode, - ) + showMakeBackupAtFirstAlertBS() } else { unlockWalletIfNeedAndProceed { authorizationRequired -> router.push( @@ -392,35 +391,28 @@ internal class WalletSettingsModel @Inject constructor( } private fun onUpgradeWalletClick() { - if (!state.value.isWalletBackedUp) { - showMakeBackupAtFirstAlertBS( - isUpgradeFlow = true, - action = WalletSettingsAnalyticEvents.NoticeBackupFirst.Action.Upgrade, - ) - } else { - unlockWalletIfNeedAndProceed { - router.push(AppRoute.UpgradeWallet(userWalletId = params.userWalletId)) - } - } + router.push(AppRoute.WalletHardwareBackup(userWalletId = params.userWalletId)) } private fun onBackupClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup()) - router.push(AppRoute.WalletBackup(params.userWalletId)) + router.push( + AppRoute.WalletBackup( + userWalletId = params.userWalletId, + isColdWalletOptionShown = false, + ), + ) } private fun onCardSettingsClick() { router.push(AppRoute.CardSettings(params.userWalletId)) } - private fun showMakeBackupAtFirstAlertBS( - action: WalletSettingsAnalyticEvents.NoticeBackupFirst.Action, - isUpgradeFlow: Boolean, - ) { + private fun showMakeBackupAtFirstAlertBS() { analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.NoticeBackupFirst( source = AnalyticsParam.ScreensSources.WalletSettings.value, - action = action, + action = WalletSettingsAnalyticEvents.NoticeBackupFirst.Action.AccessCode, ), ) val message = bottomSheetMessage { @@ -438,14 +430,10 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.CreateWalletBackup( userWalletId = params.userWalletId, - isUpgradeFlow = isUpgradeFlow, + isUpgradeFlow = false, shouldSetAccessCode = true, analyticsSource = AnalyticsParam.ScreensSources.WalletSettings.value, - analyticsAction = if (isUpgradeFlow) { - RecoveryPhraseScreenAction.Upgrade.value - } else { - RecoveryPhraseScreenAction.AccessCode.value - }, + analyticsAction = RecoveryPhraseScreenAction.AccessCode.value, ), ) closeBs() diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 7f9cb9d418..6590758780 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -42,7 +42,6 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig 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 @@ -153,10 +152,6 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) - is WalletSettingsItemUM.UpgradeWallet -> UpgradeWalletBlock( - modifier = itemModifier, - model = item, - ) is WalletSettingsAccountsUM.Header -> AccountsHeader(item, itemModifier) is WalletSettingsAccountsUM.Account -> AccountItem( model = item, @@ -211,35 +206,54 @@ private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier enabled = model.isEnabled, onClick = model.onClick, ) { - Row( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - CardImage(model.imageState) - Column(modifier = Modifier.weight(1f)) { - Text( - text = model.title.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = model.text.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, + Column { + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + CardImage(model.imageState) + Column(modifier = Modifier.weight(1f)) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + SecondarySmallButton( + config = SmallButtonConfig( + isEnabled = model.isEnabled, + text = resourceReference(R.string.common_rename), + onClick = model.onClick, + ), ) } - SecondarySmallButton( - config = SmallButtonConfig( - isEnabled = model.isEnabled, - text = resourceReference(R.string.common_rename), - onClick = model.onClick, - ), - ) + + if (model.additionalBlock != null) { + Column { + HorizontalDivider( + thickness = TangemTheme.dimens.size0_5, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12), + color = TangemTheme.colors.stroke.primary, + ) + + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = model.additionalBlock, + ) + } + } } } } @@ -272,21 +286,6 @@ private fun SwitchBlock(model: WalletSettingsItemUM.WithSwitch, modifier: Modifi } } -@Composable -private fun UpgradeWalletBlock(model: WalletSettingsItemUM.UpgradeWallet, modifier: Modifier = Modifier) { - Notification( - config = NotificationConfig( - title = model.title, - subtitle = model.description, - iconResId = R.drawable.ic_hardware_backup_36, - iconSize = 36.dp, - onClick = model.onClick, - shouldShowArrowIcon = false, - ), - modifier = modifier, - ) -} - @Composable private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermission, modifier: Modifier = Modifier) { Notification( @@ -480,20 +479,4 @@ private fun Preview_WalletSettingsScreen() { PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) } } - -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview_WalletSettingsScreen1() { - TangemThemePreview { - UpgradeWalletBlock( - model = WalletSettingsItemUM.UpgradeWallet( - id = "upgrade_wallet", - title = stringReference("Upgrade wallet with a hardware backup"), - description = stringReference("Keep your crypto safe with Tangem’s best-in-class hardware wallet."), - onClick = {}, - ), - ) - } -} // endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 86b1637320..a103dee2de 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -38,17 +38,10 @@ internal class ItemsBuilder @Inject constructor() { onReferralClick: () -> Unit, onManageTokensClick: () -> Unit, onAccessCodeClick: () -> Unit, - onUpgradeWalletClick: () -> Unit, onBackupClick: () -> Unit, onCardSettingsClick: () -> Unit, ): PersistentList = persistentListOf() .add(cardItem) - .addAll( - buildUpgradeWalletItem( - userWallet = userWallet, - onUpgradeWalletClick = onUpgradeWalletClick, - ), - ) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .addAll(accountsUM) .add( @@ -116,21 +109,6 @@ internal class ItemsBuilder @Inject constructor() { onCheckedChange = onCheckedNFTChange, ) - private fun buildUpgradeWalletItem( - userWallet: UserWallet, - onUpgradeWalletClick: () -> Unit, - ): List = when (userWallet) { - is UserWallet.Cold -> emptyList() - is UserWallet.Hot -> listOf( - WalletSettingsItemUM.UpgradeWallet( - id = "upgrade_wallet", - title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), - description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), - onClick = onUpgradeWalletClick, - ), - ) - } - private fun buildNotificationsPermissionItem() = WalletSettingsItemUM.NotificationPermission( id = "notifications_permission", title = resourceReference(id = R.string.transaction_notifications_warning_title), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt index 18d1546028..8162044ed2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.utils import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet @@ -24,6 +25,7 @@ internal class WalletCardItemDelegate @AssistedInject constructor( private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val walletImageFetcher: UserWalletImageFetcher, @Assisted private val dialogNavigation: SlotNavigation, + @Assisted private val onUpgradeHotWalletClick: () -> Unit, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -43,10 +45,20 @@ internal class WalletCardItemDelegate @AssistedInject constructor( isEnabled = isRenameAvailable, onClick = { openRenameWalletDialog(wallet) }, imageState = imageState, + additionalBlock = buildUpgradeToHardwareWalletBlockOrNull(wallet), ) }, ) + private fun buildUpgradeToHardwareWalletBlockOrNull(wallet: UserWallet): BlockUM? { + return BlockUM( + text = resourceReference(R.string.upgrade_to_hardware_wallet_button_title), + iconRes = null, + onClick = onUpgradeHotWalletClick, + accentType = BlockUM.AccentType.ACCENT, + ).takeIf { wallet is UserWallet.Hot } + } + private fun openRenameWalletDialog(userWallet: UserWallet) { val config = DialogConfig.RenameWallet( userWalletId = userWallet.walletId, @@ -57,6 +69,9 @@ internal class WalletCardItemDelegate @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(dialogNavigation: SlotNavigation): WalletCardItemDelegate + fun create( + dialogNavigation: SlotNavigation, + onUpgradeHotWalletClick: () -> Unit, + ): WalletCardItemDelegate } } \ 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 8d1c6bd283..ed19a09083 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 @@ -477,7 +477,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onFinishWalletActivationClick(isBackupExists: Boolean) { analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation()) val userWalletId = stateHolder.getSelectedWalletId() - appRouter.push(WalletActivation(userWalletId, isBackupExists)) + val route = if (isBackupExists) { + WalletActivation(userWalletId = userWalletId, isBackupExists = true) + } else { + WalletBackup( + userWalletId = userWalletId, + isColdWalletOptionShown = true, + ) + } + + appRouter.push(route) } override fun onAllowPermissions() { 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 69b492660a..4b4227a657 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,6 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils -import com.tangem.common.routing.AppRoute.WalletActivation +import com.tangem.common.routing.AppRoute.WalletBackup import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -93,7 +93,12 @@ internal class WalletWarningsSingleEventSender @Inject constructor( primaryButton { text = resourceReference(R.string.hw_activation_need_backup) onClick { - router.push(WalletActivation(userWallet.walletId, userWallet.backedUp)) + router.push( + WalletBackup( + userWalletId = userWalletId, + isColdWalletOptionShown = true, + ), + ) closeBs() } } diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 7557118256..09713985c8 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { implementation(projects.features.welcome.api) implementation(projects.features.wallet.api) + implementation(projects.features.hotWallet.api) /** Core */ implementation(projects.core.configToggles) 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 b5b89566f2..4179ffefe8 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 @@ -12,14 +12,22 @@ 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.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.common.wallets.error.UnlockWalletError 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.settings.CanUseBiometryUseCase +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.NonBiometricUnlockWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.welcome.impl.ui.state.WelcomeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,10 +48,15 @@ internal class WelcomeModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, private val walletsRepository: WalletsRepository, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, userWalletsFetcherFactory: UserWalletsFetcher.Factory, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val scanCardProcessor: ScanCardProcessor, + private val messageSender: UiMessageSender, ) : Model() { val uiState: StateFlow @@ -168,7 +181,50 @@ internal class WelcomeModel @Inject constructor( private fun addWalletClick() { analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) - router.push(AppRoute.CreateWalletSelection) + if (hotWalletFeatureToggles.isWalletCreationRestrictionEnabled) { + scanCard() + } else { + router.push(AppRoute.CreateWalletSelection) + } + } + + private fun scanCard() { + modelScope.launch { + scanCardProcessor.scan( + analyticsSource = AnalyticsParam.ScreensSources.SignIn, + onWalletNotCreated = {}, + disclaimerWillShow = { router.pop() }, + onSuccess = { scanResponse -> + val userWallet = + coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() ?: return@scan + saveWalletUseCase.invoke(userWallet) + .onLeft { error -> + if (error is SaveWalletError.WalletAlreadySaved) { + userWalletsListRepository.unlock( + userWallet.walletId, + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + ).onRight { + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) + } + } + } + .onRight { + router.replaceAll(AppRoute.Wallet) + } + }, + onCancel = {}, + onFailure = { tangemError -> + if (!tangemError.silent) { + val message = tangemError.messageResId + ?.let(::resourceReference) + ?: stringReference(tangemError.customMessage) + + messageSender.send(SnackbarMessage(message)) + } + }, + ) + } } private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9f09031069..370da97987 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-1335" +tangemBlockchainSdk = "releases-5.32-1382" #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 7f3ae26151d7e34c09bf3d0fb034eabf4ce382ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Jan 2026 17:31:34 +0400 Subject: [PATCH 145/154] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 30 +++++++++++---- core/res/src/main/res/values-es/strings.xml | 26 ++++++++++--- core/res/src/main/res/values-fr/strings.xml | 26 ++++++++++--- core/res/src/main/res/values-it/strings.xml | 2 + core/res/src/main/res/values-ja/strings.xml | 3 +- core/res/src/main/res/values-ru/strings.xml | 33 +++++++++-------- .../src/main/res/values-uk-rUA/strings.xml | 7 +++- .../src/main/res/values-zh-rTW/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 14 +++---- .../tangempay/model/TangemPayDetailsModel.kt | 37 +++++++++++++------ .../utils/TangemPayMessagesFactory.kt | 19 ++++++++++ 11 files changed, 145 insertions(+), 54 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 0a5f0530a8..6368d1bf58 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -327,7 +327,8 @@ Jetzt OK Im Browser öffnen - Hauptkarte oder Ring + oder + Hauptkarte Primärring Passphrase Einfügen @@ -437,6 +438,7 @@ Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte oder einen Ring. Er erzwingt eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls. Passcode Bevor du einen Befehl ausführst, der eine Änderung des Kartenstatus zur Folge hat, musst du den Passcode eingeben. + Upgrade auf Hardware-Wallet NFT Empfehlungsprogramm Flippe den Bildschirm deines Geräts nach unten, um Salden schnell ein- und auszublenden @@ -450,6 +452,7 @@ Signiert Gib uns eine Rückmeldung Details + Du kannst jeweils nur eine mobile Wallet haben. Sie kann zu einer Tangem-Wallet aufgerüstet oder zusammen mit einer neuen Wallet verwendet werden. Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen Standardadresse @@ -569,8 +572,8 @@ Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. Erlaubnis erteilen Unbegrenzt - Dein Backup wird mit 2 oder 3 Tangem-Karten erstellt. Bewahre diese an separaten & sicheren Orten auf, um sie vor Verlust oder Beschädigung zu schützen – es ist keine Seed-Phrase erforderlich. - Backup mit mehreren Karten + Die Adressen werden direkt auf Deiner Tangem-Hardware-Wallet generiert – sofort einsatzbereit und vollständig geschützt. + Neue Adressen Tangem Wallet hinzufügen Dein privater Schlüssel wird direkt in der Tangem-Karte generiert und verlässt diese nie. Schlüsselgenerierung @@ -611,11 +614,15 @@ 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. + Rüste Deine mobile Wallet auf eine Tangem-Hardware-Wallet um, um höchste Sicherheit zu gewährleisten. Importiere Deine Wallet oder übertrage Dein Guthaben auf eine neue Wallet. + Upgrade auf Cold Wallet + Deine privaten Schlüssel sind nur für die Tangem Hot-Wallet sicher verschlüsselt und auf Deinem Telefon gespeichert. Schlüssel werden in der App gespeichert Erstelle oder stelle Deine Wallet mithilfe einer Wiederherstellungsphrase wieder her – Deinem integrierten Backup. Sicherung der Seed-Phrase Mobile Wallet erstellen + Übertrage Deine mobile Wallet jederzeit und sicher auf eine Tangem-Karte oder einen Tangem-Ring. + Upgrade auf Hardware-Wallet Vorhandene Wallet importieren Diese Wiederherstellungsphrase wurde bereits importiert Mobile Wallet @@ -633,14 +640,14 @@ 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 + Alle Deine Adressen und Guthaben bleiben beim Upgrade Deiner mobilen Wallet auf eine Tangem-Karte oder einen Tangem-Ring vollständig zugänglich. + Gleiche Adressen Ein Upgrade ist nicht möglich. Auf diesem Gerät ist bereits eine Wallet vorhanden. Wähle ein anderes Gerät. Dieses Gerät kann für das Upgrade nicht verwendet werden. Bei der Operation ist ein Fehler aufgetreten. Dein Geld bleibt während des gesamten Prozesses sicher und vollständig zugänglich Zugang zu Geldern - Die Daten Deiner Wallet werden aus der App gelöscht und auf Deinem Tangem-Gerät gespeichert + Nach dem Upgrade wird Deine mobile Wallet aus der App entfernt und auf Deine Tangem-Hardware-Wallet gespeichert. Deine Wiederherstellungsphrase bleibt erhalten. Allgemeine Sicherheit Die privaten Schlüssel werden von der App auf Ihr Tangem-Gerät übertragen. Schlüsselmigration @@ -1242,6 +1249,11 @@ Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. + Einfach zu bedienen + Schützt Deine Kryptowährungen und bewahrt diese offline auf. So schlank wie eine Kreditkarte, sicherer als ein Banktresor. + Keine Seed-Phrase + Erstklassige Hardware-Wallet + Tangem Cold Wallet Name Lass deinen Token für dich arbeiten Eine Netzwerkgebühr ist eine kleine Zahlung, die erforderlich ist, um Deine Transaktion auf der Blockchain zu verarbeiten und zu bestätigen. @@ -1542,6 +1554,8 @@ Tangem Pay ist vorübergehend nicht erreichbar. Tangem Pay Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen + Sie können nur USDC Polygon von Ihrer On-Chain-Adresse abheben. Ihr On-Chain-Guthaben unterscheidet sich von Ihrem Kartenguthaben und wird innerhalb von 2 Werktagen nach einem Kauf aktualisiert (bei Rückerstattungen oder stornierten Transaktionen kann es bis zu 14 Werktage dauern). + Bitte beachten Sie Ihr PIN-Code Das ist meine Wallet Guthaben versteckt @@ -1613,6 +1627,7 @@ Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. + Upgrade auf Hardware-Wallet Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten Echtzeit-Warnungen für Transaktionen, Umtausch und wichtige Aktualisierungen. Transaktionswarnungen @@ -1982,6 +1997,7 @@ Schnelle Lieferung Mit einem Fingertipp starten Nahtlos und sicher + Keine Seed-Phrase Einfach zu bedienen Erstelle eine Hardware-Wallet mit Tangem. Schlank wie eine Bankkarte, sicher wie ein Banktresor. Erstelle oder importiere eine Software-Wallet diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 70a5296c78..29ba81c3de 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -327,6 +327,7 @@ Ahora OK Abrir en el navegador + o Tarjeta principal Anillo primario Frase de contraseña @@ -437,6 +438,7 @@ Este mecanismo protege contra los ataques de proximidad a una tarjeta o anillo. Aplicará un retardo entre la recepción y la ejecución de un comando. Contraseña Antes de ejecutar un comando que cambie el estado de la tarjeta, deberá ingresar una contraseña. + Actualizar a billetera de hardware NFT Programa de referidos Gire la pantalla de su dispositivo hacia abajo para ocultar y mostrar rápidamente los saldos @@ -450,6 +452,7 @@ Firmado Enviar comentarios Detalles + Puedes tener solo una billetera móvil a la vez. Puede actualizarse a una billetera fría Tangem o usarse junto con una nueva billetera fría. Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Dirección por defecto @@ -569,8 +572,8 @@ 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 + Se generarán nuevas direcciones dentro de su billetera física, listas para su uso y totalmente seguras. + Nuevas direcciones Agregar billetera Tangem Su clave privada se genera directamente dentro de la tarjeta Tangem y nunca sale de ella. Generación de claves @@ -611,11 +614,15 @@ 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. + Actualice su billetera móvil a una billetera de hardware Tangem para obtener la máxima seguridad. Importe su billetera o transfiera fondos a una nueva. + Actualizar a billetera fría 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 + Transfiera su billetera móvil a una tarjeta o anillo Tangem en cualquier momento, de forma segura. + Actualización a billetera de hardware Importar billetera existente Esta frase de recuperación ya ha sido importada Billetera móvil @@ -633,14 +640,14 @@ 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 + Todas sus direcciones y saldos permanecen completamente accesibles al actualizar su billetera móvil a una tarjeta o anillo Tangem. + Mismas direcciones 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. + Después de la actualización, su billetera móvil se eliminará de la aplicación y se almacenará en su billetera de hardware; su frase de recuperación permanecerá bajo su control. Seguridad general Las claves privadas se transferirán de la aplicación a su billetera de hardware Tangem Migración de claves @@ -1242,6 +1249,11 @@ Escanee la tarjeta/anillo que quiere configurar Olvidar la billetera Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. + Fácil de usar + Mantenga sus criptos seguras y sin conexión. Tan delgadas como una tarjeta de crédito, más seguras que una bóveda bancaria. + Sin seed phrase + La mejor billetera de hardware de su clase + Billetera Fría Tangem Nombre Haga que su token trabaje para Ud. Una tarifa de red es un pequeño pago necesario para procesar y confirmar su transacción en la blockchain. @@ -1542,6 +1554,8 @@ Tangem Pay temporalmente no disponible Tangem Pay Haga clic en el botón de abajo para restaurar el acceso + Solo puedes retirar USDC Polygon disponibles en tu dirección on-chain. El saldo on-chain difiere del saldo de tu tarjeta y se actualiza en 2 días hábiles después de una compra (para reembolsos o transacciones canceladas puede tomar hasta 14 días hábiles). + Tenga en cuenta Tu código PIN Esta es mi billetera Saldos ocultos @@ -1613,6 +1627,7 @@ Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte. Use %s o escanee una tarjeta/anillo para tener acceso a su billetera Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. + Actualización a billetera de hardware Manténgase actualizado con las últimas funciones y noticias Alertas en tiempo real de transacciones, intercambios y actualizaciones críticas. Alertas de transacciones @@ -1982,6 +1997,7 @@ Entrega rápida Empiece con un solo toque Fácil y seguro + Sin seed phrase Fácil de usar Cree una billetera de hardware con Tangem. Delgada como una tarjeta bancaria, segura como una bóveda. Crear o importar una billetera de software diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a1a06768bb..f0b5703f1c 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -326,6 +326,7 @@ Maintenant OK Ouvrir dans le navigateur + ou Carte principale Bague principale Passphrase @@ -434,6 +435,7 @@ Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. + Passez au hardware wallet NFT Programme de parrainage Retournez l\'écran de votre appareil vers le bas pour masquer et afficher rapidement les soldes @@ -447,6 +449,7 @@ Signé Envoyer un commentaire Détails + Vous ne pouvez avoir qu\'un seul portefeuille mobile à la fois. Il peut être mis à niveau vers un hardware wallet Tangem ou utilisé avec un nouveau hardware wallet. Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation Adresse par défaut @@ -566,8 +569,8 @@ 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 + De nouvelles adresses seront générées pour vous dans votre hardware wallet, prêtes à l\'emploi et entièrement sécurisées. + Nouvelles adresses 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 @@ -608,11 +611,15 @@ 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. + Mettez à niveau votre portefeuille mobile vers un portefeuille matériel Tangem pour bénéficier d\'une sécurité maximale. Importez votre portefeuille ou transférez vos fonds vers un nouveau portefeuille. + Passez à un hardware wallet 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 + Transférez votre portefeuille mobile vers une carte ou une bague Tangem à tout moment, en toute sécurité. + Passez à un hardware wallet Importer un portefeuille existant Cette seed phrase a déjà été importée. Wallet mobile @@ -630,14 +637,14 @@ 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 + Toutes vos adresses et tous vos soldes restent entièrement accessibles lorsque vous passez d\'un portefeuille mobile à une carte ou une bague Tangem. + Mêmes adresses 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. + Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession. 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 @@ -1238,6 +1245,11 @@ Scannez la carte/ bague que vous souhaitez configurer. Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. + Facile à utiliser + 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. + Pas de seedphrase + Le meilleur hardware wallet + Tangem Hardware Wallet Nom Faites travailler votre jeton pour vous Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking. @@ -1535,6 +1547,8 @@ Tangem Pay est temporairement indisponible Tangem Pay Cliquez sur le bouton ci-dessous pour restaurer l\'accès + Vous ne pouvez retirer que les USDC Polygon disponibles sur votre adresse on-chain. Le solde on-chain diffère du solde de votre carte et se met à jour dans les 2 jours ouvrables après un achat (pour les remboursements ou transactions annulées, cela peut prendre jusqu\'à 14 jours ouvrables). + Veuillez noter Votre code PIN C\'est mon portefeuille Soldes masqués @@ -1605,6 +1619,7 @@ Nous avons rencontré une erreur. Code d\'erreur : %s. Veuillez contacter notre équipe de support. Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion. + Passez à un hardware wallet Restez à jour avec les dernières fonctionnalités et actualités Alertes en temps réel pour les transactions, les échanges et les mises à jour importantes. Alertes de transaction @@ -1968,6 +1983,7 @@ Livraison rapide Commencez en un seul clic Sans faille et sécurisé + Pas de seedphrase Facile à utiliser Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort. Créer ou importer un portefeuille logiciel diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index cfa982d89a..ca85a31ba2 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -174,6 +174,8 @@ Tangem Pay è temporaneamente non disponibile Tangem Pay Fare clic sul pulsante in basso per ripristinare l\'accesso + Puoi prelevare solo USDC Polygon disponibili sul tuo indirizzo on-chain. Il saldo on-chain differisce dal saldo della carta e si aggiorna entro 2 giorni lavorativi dopo un acquisto (per rimborsi o transazioni annullate può richiedere fino a 14 giorni lavorativi). + Attenzione 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 565f5cf8f8..e10e9e29ae 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -367,6 +367,7 @@ 対応ネットワーク スワップ Tangem + Tangem Wallet 利用規約 利用規約 宛先 @@ -1547,7 +1548,7 @@ Tangem Payは現在一時的に利用できません。 Tangem Pay 下のボタンをクリックしてアクセスを復元してください - 現在引き出せるのは、保留中の取引分を差し引いたオンチェーンアドレス上のUSDC(Polygon)のみです。以前に返金や購入のキャンセルを行った場合、暗号資産アカウントの残高は2営業日以内に更新されます。 + オンチェーンアドレスにある USDC Polygon のみ出金可能です。オンチェーン残高はカード残高とは異なり、購入後 2 営業日以内に更新されます(返金またはキャンセルされた取引の場合、最大 14 営業日かかることがあります)。 ご注意ください PINコード これは私のウォレットです diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ef16028c2d..fceb5bf0dd 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -133,9 +133,9 @@ Эти слова невозможно восстановить, если они будут потеряны. Храните их в надёжном месте. Бэкап завершен Ваша фраза восстановления — фиксированный набор из %s случайных слов для доступа и восстановления кошелька. - Эти слова невозможно восстановить, если они будут потеряны. Храните их в безопасности. + Если вы потеряете эту фразу, восстановить её будет невозможно. Храните её в полной безопасности. Храните в безопасности - Сохраните эти %s слов в безопасном месте и никому их не сообщайте. + Сохраните эти %s слов в надежном месте, к которому только вы имеете доступ, и никому их не показывайте. Восстановление невозможно Фраза восстановления Никому не сообщайте эти слова. Tangem никогда не будет их спрашивать. Ниже приведены %s слов вашей фразы восстановления кошелька. Используйте их, чтобы восстановить кошелёк в случае потери устройства. @@ -384,7 +384,7 @@ Поддерживаемые сети Обменять Tangem - Tangem Кошелек + Tangem Wallet условия участия Условиями использования На @@ -466,7 +466,7 @@ Подписано Отправить отзыв Подробности - Одновременно у вас может быть только один мобильный кошелек. Его можно апгрейдить до Tangem cold wallet или использовать вместе с новым холодным кошельком. + У вас может быть только один мобильный кошелек. Вы можете апгрейдить его до аппаратного кошелька Tangem или добавить новый аппаратный кошелек. Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования Основной адрес @@ -586,7 +586,7 @@ Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно - Новые адреса будут сгенерированы прямо в вашем аппаратном кошельке, готовы к использованию и полностью защищены. + Адреса будут сгенерированы прямо в аппаратном кошельке Tangem — готовы к использованию и полностью защищены Новые адреса Добавить кошелек Tangem Ваш приватный ключ будет сгенерирован на карте Tangem и никогда не покинет её. @@ -629,13 +629,13 @@ Фраза восстановления Чтобы защитить ваш кошелёк с помощью кода доступа, сначала завершите резервное копирование. Чтобы улучшить кошелёк до аппаратного, сначала создайте резервную копию. - Перенесите свой мобильный кошелек на аппаратный кошелек Tangem для максимальной безопасности. Импортируйте существующий кошелек или переведите средства на новый. + Сделайте апгрейд до аппаратного кошелька Tangem для максимальной безопасности.\nИмпортируйте существующий кошелек или создайте новый. Апгрейд до холодного кошелька - Ваши приватные ключи надёжно зашифрованы и хранятся на вашем телефоне + Ваши приватные ключи надёжно зашифрованы и хранятся на вашем телефоне. Ключи хранятся в приложении Создайте или импортируйте кошелёк с помощью вашей фразы восстановления. Резервная копия - Создать мобильный кошелек + Мобильный кошелек Перенесите свой мобильный кошелек на Tangem-карту или кольцо в любое время. Апгрейд до аппаратного кошелька Импортировать существующий @@ -662,15 +662,15 @@ Во время операции произошла ошибка. Ваши средства остаются в безопасности и полностью доступны в процессе. Доступ к средствам - После апгрейда ваш мобильный кошелек будет удалён из приложения и сохранён на аппаратном кошельке; фраза восстановления остаётся у вас. + После апгрейда мобильный кошелек удаляется из приложения, ключи сохраняются в вашем аппаратном кошельке Tangem. Seed-фраза остаётся у вас. Общая безопасность - Приватные ключи будут перемещены из приложения в вашу Tangem карту или кольцо + Приватные ключи будут перемещены на вашу Tangem-карту или кольцо. Миграция ключей Сканировать устройство Начать апгрейд Вы собираетесь перейти на устройство Tangem, где ваши активы будут в безопасности в холодном хранилище. Tangem Wallet - Обновиться до аппаратного кошелька + Апгрейд до аппаратного кошелька Храните свою криптовалюту в безопасности с помощью первоклассного аппаратного кошелька Tangem. Сделать апгрейд кошелька до аппаратной версии. Эта информация была сгенерирована ИИ.\nНажмите здесь, если обнаружили ошибку. @@ -1028,6 +1028,7 @@ Восстановление кода доступа Идентичные карты Код доступа + У вас может быть только один мобильный кошелек. Вы можете апгрейдить его до аппаратного кошелька Tangem или добавить новый аппаратный кошелек. Все предложения Доступно с %s Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами @@ -1291,7 +1292,7 @@ Прост в использовании Хранит вашу криптовалюту в безопасности и офлайн. Тонкий, как кредитная карта, надежнее банковского сейфа. Без seed-фразы - Лучший в своём классе аппаратный кошелек + Лучший в своём классе Холодный кошелек Tangem Имя Заставьте ваш токен работать @@ -1593,6 +1594,8 @@ Tangem Pay временно недоступен Tangem Pay Нажмите на кнопку ниже, чтобы восстановить доступ + Вывод доступен только для USDC Polygon на вашем ончейн-адресе. Баланс ончейн-адреса отличается от баланса карты и обновляется в течение 2 рабочих дней после покупки (при возвратах или отменах транзакций — до 14 рабочих дней). + Обратите внимание Ваш PIN-код Это мой кошелек Балансы скрыты @@ -1675,7 +1678,7 @@ Хотите использовать Push-уведомления? Включите push-уведомления, и мы мгновенно сообщим вам, когда поступят средства. Не пропустите транзакцию - Добавить новый кошелек + Добавить кошелек Если вы удалите этот кошелёк без резервной копии, вы навсегда потеряете доступ к своим средствам. Вы уверены, что хотите забыть этот кошелек? Произошла ошибка, пожалуйста, отсканируйте свою карту или кольцо для входа @@ -1970,9 +1973,9 @@ URI уже используется WalletConnect Подозрительная транзакция - Уже есть Tangem кошелек? + Уже есть кошелек Tangem? Тысячи активов - Лучший в своём классе аппаратный кошелёк + Лучший в своём классе Быстрая доставка Начните в один клик Просто и надёжно 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 5954c2933e..bcde67362a 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -382,6 +382,7 @@ Підтримувані мережі Обмін Tangem + Tangem Wallet умови участі Умовами використання До @@ -623,7 +624,7 @@ Ключі зберігаються у застосунку Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення. Резервна копія - Створити мобільний гаманець + Мобільний гаманець Імпортувати існуючий гаманець Ця фраза відновлення вже була імпортована Мобільний гаманець @@ -1569,6 +1570,8 @@ Tangem Pay тимчасово недоступний Tangem Pay Натисніть кнопку нижче, щоб відновити доступ + Виведення доступне лише для USDC Polygon на вашій ончейн-адресі. Баланс ончейн-адреси відрізняється від балансу картки та оновлюється протягом 2 робочих днів після покупки (у разі повернень або скасувань транзакцій — до 14 робочих днів). + Зверніть увагу Ваш PIN-код Це мій гаманець Баланси приховано @@ -1650,7 +1653,7 @@ Бажаєте використовувати Push-повідомлення? Увімкніть 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 393b0cadb9..8f05307960 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -416,6 +416,8 @@ Tangem Pay暂时不可用 Tangem Pay 點擊下方按鈕以恢復存取權限 + 您僅能提領鏈上地址中的 USDC Polygon。鏈上餘額與卡片餘額不同,購買後將在 2 個工作日內更新(若有退款或取消交易,最多需 14 個工作日)。 + 請注意 您的PIN码 隱藏 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f7732464c2..3cf0a8eaec 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -131,9 +131,9 @@ These words cannot be recovered if lost. Store them securely. Backup completed 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. + If you lose this phrase, it cannot be recovered. Keep it completely secure. Keep it safe - Save these %s words in a secure location and never share them with anyone. + Save these %s words in a secure place that only you can access, and never share them with anyone. No recovery possible Recovery phrase 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. @@ -453,7 +453,7 @@ Signed Send feedback Details - You can have only one mobile wallet at a time. It can be upgraded to a Tangem cold wallet or used alongside a new cold wallet. + You can only have one mobile wallet. Upgrade it to Tangem hardware wallet, or add a new hardware wallet. Check your internet connection or switch to a different network Terms of service Default Address @@ -576,7 +576,7 @@ To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited - New addresses will be generated for you inside your hardware wallet, ready for use and fully secure. + Addresses are generated directly on your Tangem hardware wallet — ready to use and fully protected. New Addresses Add Tangem Wallet Your private key will be generated directly inside the Tangem card and will never leave it. @@ -652,7 +652,7 @@ An error occurred during the operation. Your funds remain safe and fully accessible during the process Access to funds - After upgrading, your mobile wallet will be removed from the app and stored on your hardware wallet; your recovery phrase stays with you. + After upgrading, your mobile wallet is removed from the app and stored on your Tangem hardware wallet. Your recovery phrase stays with you. General security Private keys will be moved from the app to your Tangem hardware wallet Key migration @@ -1000,7 +1000,7 @@ Access code restore Identical cards Access code - You can have either a mobile wallet or a hardware wallet at one time. Upgrade your wallet or delete it from the app to start using a Tangem cold wallet. + You can only have one mobile wallet. Upgrade it to Tangem hardware wallet, or add a new hardware wallet. All offers Available with %s Providers facilitate transactions @@ -1568,7 +1568,7 @@ Tangem Pay is temporarily unreachable Tangem Pay Click the button below to restore access - You can only withdraw USDC (Polygon) currently available on your on‑chain address balance, taking into account pending transactions. If you previously made a refund or cancelled a purchase, your crypto account balance will update within 2 business days. + You can only withdraw USDC Polygon currently available on your on-chain address. Your on-chain balance differs from your card balance and updates within 2 business days after a purchase (for refunds or canceled transactions it may take up to 14 business days). Please note Your PIN code This is my wallet 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 2c2b69be13..8f7b249781 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 @@ -21,6 +21,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayTopUpData @@ -258,18 +259,9 @@ internal class TangemPayDetailsModel @Inject constructor( .getOrNull() } if (currency != null) { - router.push( - AppRoute.Swap( - currencyFrom = currency, - userWalletId = params.userWalletId, - isInitialReverseOrder = false, - screenSource = AnalyticsParam.ScreensSources.TangemPay.value, - tangemPayInput = AppRoute.Swap.TangemPayInput( - cryptoAmount = currentBalance.availableForWithdrawal, - fiatAmount = currentBalance.availableForWithdrawal, - depositAddress = depositAddress, - isWithdrawal = true, - ), + uiMessageSender.send( + message = TangemPayMessagesFactory.createWithdrawWarning( + onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, ), ) } else { @@ -280,6 +272,27 @@ internal class TangemPayDetailsModel @Inject constructor( } } + private fun onConfirmWithdrawal( + currency: CryptoCurrency, + currentBalance: TangemPayCardBalance, + depositAddress: String, + ) { + router.push( + AppRoute.Swap( + currencyFrom = currency, + userWalletId = params.userWalletId, + isInitialReverseOrder = false, + screenSource = AnalyticsParam.ScreensSources.TangemPay.value, + tangemPayInput = AppRoute.Swap.TangemPayInput( + cryptoAmount = currentBalance.availableForWithdrawal, + fiatAmount = currentBalance.availableForWithdrawal, + depositAddress = depositAddress, + isWithdrawal = true, + ), + ), + ) + } + private fun fetchBalance(): Job { return modelScope.launch { val result = try { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt index 81a80c81fe..0aff2907e9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -94,4 +94,23 @@ internal object TangemPayMessagesFactory { } } } + + fun createWithdrawWarning(onGotItClick: () -> Unit): BottomSheetMessageV2 { + return bottomSheetMessage { + infoBlock { + icon(R.drawable.img_attention_20) { + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention + } + title = TextReference.Res(R.string.tangempay_withdrawal_note_title) + body = TextReference.Res(R.string.tangempay_withdrawal_note_description) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { + onGotItClick() + closeBs() + } + } + } + } } \ No newline at end of file From 723c6159d5844fae6ea09a68206c5b68c33cc5ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jan 2026 12:14:37 +0300 Subject: [PATCH 146/154] Updated on 2026-08-14 --- .../wallet/domain/Wallet2CobrandImage.kt | 10 ++++++++-- .../res/drawable/ill_lunar_card2_120_106.webp | Bin 0 -> 24208 bytes .../res/drawable/ill_lunar_card3_120_106.webp | Bin 0 -> 35758 bytes 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_lunar_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_lunar_card3_120_106.webp diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index bda79ff305..27856a0954 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -305,7 +305,7 @@ internal enum class Wallet2CobrandImage( USA( cards2ResId = R.drawable.ill_usa_card2_120_106, cards3ResId = R.drawable.ill_usa_card3_120_106, - batchIds = setOf("AF91", "AF990017"), + batchIds = setOf("AF91", "AF990017", "AF990056"), ), VeChain( @@ -384,6 +384,12 @@ internal enum class Wallet2CobrandImage( HyperBlue( cards2ResId = R.drawable.ill_hyperblue_card2_120_106, cards3ResId = R.drawable.ill_hyperblue_card3_120_106, - batchIds = setOf("AF990026", "AF990027", "AF990028"), + batchIds = setOf("AF990026", "AF990027", "AF990028", "AF990050", "AF990051", "AF990052"), + ), + + Lunar( + cards2ResId = R.drawable.ill_lunar_card2_120_106, + cards3ResId = R.drawable.ill_lunar_card3_120_106, + batchIds = setOf("AF990057", "AF990058", "AF990059"), ), } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_lunar_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_lunar_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..b550f44d65162dc777eda91b20b5419ab52b7939 GIT binary patch literal 24208 zcmb4JQ*18|te$P#wr$(CcjoQPwr$(C?d{CAZ5zM)f4VRC={9N7^r30KCSRMjD$)`X z5e7g&n&Ki#8cJMRFhD>+NdM^`Fi<%#kg%MRlnDe75IkP1-4@d$z@L}jS&WRniE5ve zJBh5jZP8gk$HuYsaXm;6h-1p=Z6vqR(P29)50jI`sL@VWS_Ozv#pTv_ExY65dkGk5 zAc0KXw2b}F^z(BYJ6{j_hQ@QEokZKuMtPeZZlay;QVk~rySla*m*du0;`NpLQd^0K z+b!@AZaE<5*ZFhqLhyf$X4oin*EJRE)a!8v0gtQb95P|e?1v_%bE+e>??D;;8!AIS z$B*=cZ#wW%4mCl%S~l=|hvSx$YzNP;k9=f3?XtzD?gJ{cDS$Cv%T9aub6Utq53hs7 zRB<0aa`7Jj#a6K&In|j$^k1Y{K0^hH^SR8Ow#_?P6q+fH7iITMa@7qql2&?mzEAHU zCtxor=*$qipi`bi^D3fnRl=bI#C|EPwjhU%VBw0T{^RB(4%)AL zZ4?nc1~xcB0tgEp=f4vaggt>j#=5;XW6Ilg!08TVr zO&((=7d8$6qdM$C+m?X;<(UUio3Xc3KdmNfNsgt7%6C02YFZ5wRL&(p-RMNWUGSE> zV}Gix*B)(akHZF8&F!fDXfD?GRKYYYS8y3an`p1#q=mZwogp6_~_9lZ#yX1?R< z?VCKOTv>y-Rk#q(UY7Ct;6UJI+xA`;pgw&&?>>rGYc2XzW}?xyShn8>uA#wnhz{C6 zQ-W0Fkv$VP)HS7kL6U`NpZVLgP!I>XxNz#w*Q`X5Zbp00zhwp+fN0C${a09y;bQh5 z(e-`5(ZpdPJfXfWMU$Sk14x& zT3%(i-#(R!OdcRiS^fKWfOUf;IHQA*Scf8G`(@QoeVW*y?vV-^#6Gz98^wq+69ixD zN(^S7O502-YB4YyutdX$;!B$IlwV?#^eufegpxoXM-9AjxFsXh=JAM$#Ng+^1xvQ= z06;!Cw87JnLEBH?ADeM-@pJ_UMyO-{JjOE+MlsZjbLPpC=0k9 z1KeHA2@9@l_Q|JNdtQl7V0XJ@@ORX_1Kg8T&dBlRLicHXnCt72$Jg4M9Q0cT$*!+t zAq-8)&nbxC^v2vbTper=vpd`k4L9E4YhCK0?ov{JZ=$W38x^hxxBi{vcDWk}y=$E0 z`L3ao0n>SCQUG2@fzl4nbzRr}2?F2rD88QgN8!H*tiv$ze}y1M>!CX(PR6!234M|I>uQhm^PG4w?}sPV5EK zc<<~{>h^`4wnsG9auS$@?!z6%(9l%iOJg?^WmL0_m0ReOBIw?ib6YFms%W<|yVjNo z*eBEbiUu9iuTMStW%PcO*WnMj;dZvvZ7Gf%SyvKleZ8`=fgx$so%;%}6l#fs36G+7 zkuE0u!wdPxVP0Rx$3vJ*pyvInJl);9Mw0i}rZpkMa5kKz;c&liWx#{OPR;NudWU9F z9K(;gX5u#J&cTT6WUp$7CfVDndR-d=YF~Ao|ABX1wEq{yeVyDCZ{$77iZKiR7sY=t zAB=c!2=o|yqJY2mW!E?Umq_ste;@i@YGnPqd9#DQ8V=DH%zvzy?Vx`GMEYOvAp+6@ z0-6^A&IP8~2d)4XV8)6PEg~*1BvZ}GaD;%CpIKiJbfz3>N;~)OpT!>qM5p9wY@ekm zn(y*35beYe1#x^Z63*Daj$1zXuK|Q^0IA<+?}WGhP5uY{?tZq{g8PDPezm_hcaS~y zwNJE9`OAO}falL8fc(k%%l#|<+V4`Z!ruju_yKqZ{Ve|d`nG&iw25C+kZX0AKOz^?wQX{1*Wi0O;TTW7v9sFY@pD+q+}IF27@c?@!~O@h`*&K%8Ic zchK+nSNo0ofaHncl;9}9;h_NhWBjH5m2lU8+;7~c)sOQV@sXg(AO4s2 zr@aI5it%G_HvY*_9I*e8`s85baN)nxe+3A6%LY&a!awU@0UZYK0)>E;U+*8wiS(!X zoBn6Pac?&O;LZ4}9RS!Few_n+a5PQ7q*>N9^P6X0(amXFculdcYUlm`oo3-Xro{IH z-*ty?9RJ+rs!6DZu!#3n-vqym9?LCLdHE`AAxz+I+Ekc_Efje|;u_gTni$--aI)bW ziR;th{YJwXU4?}2%?^u*iQUKmk-LYD9Ar+7tr0ZP2RD(#n13AJ`Te164dfVDNPoxj zZVv6JS!=FQ2yX=3>}glIIp&-3{(YAmOn|304Dl=W$bkVWz^!AO{mwqcXIF|(<2=o! z1uq#Tb(-Wf8MFS*btLCNT|aTLh%iW*AWvEeRj{qUK7V}%0p-*g3#biQQ3reURa$8? z*P7+M5#+s*lm^$da*{scg6Gi>j*SA-0@CBh(FEwi3(|1Ix9X;DhYBb_W>m_e%Fw{T3$>0FP@5M+<1pwT=m17ms(@-Mfx0Vs0S`|*y%cp*SXFnSId!Am@HuOZyz2!G ziOIPg!^4r(7pbrJM)n5H`}GI=L}BC=3GJUOtRegTeCf`LuWh%tNRSk4QtbTx#RSJ3 z!iqtF9@;r?biM-rjczCQ5Tx4bqaF1tY(ZlkkW!w*`)3E|QipQt4{!9tv=e^W^ar== zDaTyH`!~NpTvGH1R^b*h*kKfp*_3e^#4N)(D2SVWWzAD5&VL=^d4~5}+x=Ur(7m`|1q- zcpZNG;pv>|$8xh!Wt@EDOFSmk=1a-5(GqNGlpE#ED*Jx>ECe}3grW1J*>!06)0dp8 ze?c%>fhOQ$&-eSg88~asjN;9(HhOwQ*=S*~RZ&3%=D?v;c`w0@H$JDE;7MOIjy2-JMJR9s75_4a zgOLz7GD4yw!#`D>m*@;ABPC@lxnjSt`W2n|l2H@6f_xlFc_uZ+SHk9NRbYmiu7KBN z?x_M_O*;;zvP9HJ0g}~c5~pL=vzE5{!w2~$qd~6_I-4H?iM6XWejq{lO|l)N8#b7X zctT)$GO@3cb`e*;1BYcxR1CGorES2$NUBmhx2RPlELqDw|vHd-uY4f;BsF_;FQ z5Xm6@pEeshHaxQa?3rdmdqLJLTa@MkaHa*#^CyxXpjCcg6!$VfAay@B))2J| zpsC1(GH8C}I#-X1d5eySZ}2pl+$)-`(y-5y`5E0=IGwnPJJi1=zdnvY=JS~4@7K)+ zIPj`HK(Fw?g~kZ-k3L%7tc z&8>e?`SuMTEI}!H)S{amhi~HT$DMxlBW?-K2-3v2T^j^zJxfA~=i4fIY;}h*ar!`z+u|SJ!%U$wg(9$J&rHjd<$FE7`1#v8 zze%_wzB$PcI<*$Z&rJtXKWZA2myk>RjpLf!(&obG5lcflMs?0qtpS?VmF1jIGFHDF znWsV_CE2P8WwsTPHQ(pNiw!nYWcs}*1+QM%7volvz0H}xqZ#_hnICP%QHF) z|JjVu*Un!m1pt1DdpwYK1x%7{{=9^*a(<~1e(geWF2~*-h*h+a|C#p0;03za*8Ci%Q{={&II%UCNjcF&!fSc+m?pa`HPZ(zm5^2Gjj>!BBW`5s!Ng zliB?=XZojV$+s_x4jVVq{!J;BFJ&mRRojX;VPnDumc3*|$1fgZMtQ6@siNw}4jHde z(3w`%Ij%Sse|hH&8U%FA%-b}(@%)~xAs}XG9Cl0kha(%%S`y!1!931KS@>bjrgA1c zJ7m7YGK|QGwtU=AEpAw8vJ+?Z`t(Tj$PvhvRJ;O(mh0-!>Ph?**OL#Xc_cyRMg-Vf zrSay$fb(QC>Q(+r?NmqB1{;{FeV>b?xpEQy^`*Sm%CQ5gO4c+W~R%9zDs?1V{R zEs0|!elzM}4Z%l*_Ce-W0&Fg(Zfnh{(w#hLi|=Cedg)zIq6JYDw)Q}XW&r*E;uHP1 zp7_542|#rRtoj39k^z8~EUd>K$8WX>tKg>konK2;^=A1;$=IzZTh)1J-&s(%kHv)v zS9_z~q!+ZE`yF-H`A%ToFZym-1HdCiX|KE4KUx^d^`lf4xZzmt$V;-`N1+nxC+39w zBQBwQn=^e0t2bpmS_t4bFB`6byQ%jECff_z^bJgxSBTGMZt6$RL{Hi`;Z)mJtrljG z?7~WPSk&DJ{7tlnConkw56k?K1;2W+sw)+V%A)o>y^b} z<)+$dqL;H#!;r-9);W301T1ph3(UXBYSHx=S5>C4{3rx?CDU-0kDM)|CbyOqK zHb=pET03JD@@O=^Z66%f(QduUGOA-pE1B}PAS(5BOs9Jj(oC${fV{I= zLXboz7|H7JQl)u)PHhq#YC-xm=eG9Af@kJhuG2J6IEK@435=0#zw`{1hfG^x@ij zU|4#5+34hSKOx?*73Tv2AC@oR*jeRB|8zte6^Vk^^g@JANhgHzsX)0*vYzlC zmeH$zW@6Q=g6a5(@Dtj4VMo*9Gv>q}$LZ7gnZc-Qd$L=JVwq7u_^n{S;BtNe$sl|$ zw{2E;y)`5P`-N0-gZ(yEU(n%Oz%EoAZeOcstIjX4pkpCVsb(mqwAE^cND-C&NsmZD z%fU=?<&u%Z_ZaszIlh+p9HK;BC#WW9w9L^1{Ypd--c56hP=wY5^a<=Qu=qB6ZoAU# z20uv#w6eFz!TTt%F|2fM9i-NrWFq?1JB+*W0Jp3&1zC}*j~DKkaF;cqQbjY@BZTJ#<%ZAhV=<(5I4q?sz+8bV?n5pMm1fEYa5b&@-GwN{zEoI%@T zK>`03%_xcx^aD$&Y(7M%TkVX_u3SsL_|@chWE;7m!7{B~Wsa3TM1 z7P9Sii!xw_GMEHm@Ly6;3=AGK)kOL61x-eNVSjIUbuu!HuVN<>9#d$-+f9QB1$t*E z={6P=flDsFQ{C|r020V?x?s&EgCe=pk9)2I;ldZBQ9Hn!tP~S-xFu!zF{sMLkfZ4P zZQvYMJAOCx$a{}~o@NlM2tCF36>GhC{)nf=?sL@ZGa@!z$}|m}M3J9@@hZW9UN^Z` zRJrrICEU|!fk|x(->-(yP~8gW7)Glp#r5>Qio%FDt)yL&$DTD7`I>-MAP%n#BD+~SRd2-|XJBb#X8d?=ijc>WpUZ8P^`YV6bZ z`#Dr~w&>nm?gKj4IXkkp@^CF<+@6+=a$g^$7=MVDl7Uu3}?W6&Sx|UK>l`-9K@RxHy@b zOL?*WPKq!aa?(mKmFkdoRacM@M7eHPh_gLaHD-sVn`-(d?VaM>t#aW~lq}5lmu3dy zR_=t7spUYK6s1O-$p-Mr%2OAe-H$Nc^^;jw=-Z>|8t9hka&gOAKWKk?quK^s79IE~ zsI`sJAn)DxEuZGVvoTN<^0{$0WQWAVRre@(!4O|kqFxzdskv@sVE)2n&=79mkVG>YrITV@zi**e#6e3fLKlMZvCCH>2*Jf336CnqvrlKy5u(_a z#P2z8j3xh$*3yh=zgS#2Rn>|26~X16*i2?MB?%r^GRYs>V99zYE^;J5w)dwr0Uneb z5pHkf!>T`cju}hU%g*)MF~1!g!(K);LaC(P{jPKE2y2{)oQ8;S*=^xag*o2aYEMrL z!HTU_%l%ZAc-FZ=h6JhfUjCWy=(H^stJk}JwvWItkTy z6Qjmr!k-dQ^SC@+A}_{r8JP#!~EemYk162?Hd^7)#$ntr3M)4|l?oA2~A zjG&yuPHE zknL(ZB(zy!KZ{pqHS}e8ABP)#JI*FpjFn4jp&B8JrYKO!p9skmNza*JIMZo_^Pd8s z@l4sW07JGUwGrkB$EJYCuP6-K6@YTYRgOA8hbb$4VQ)gR(peQd*uFT01l3WB6F!r; z^)(kCewqsN$Za4Q@`uHZ6axVeMz+fvcQx7$o1^M6p7ZQtYH)0yUbll_zb< zowjuwbw20ExxVuhM2f1M!B0(8fsFg~9K4APhm*EE2;RmOl9On(unG@V47ku83wt8( zwWXeI4?g(hm$t_dI2C6i1r6yXJb5pP6o=i5ilc^of^5;K4dz52cCZ&YrGfDblgKXP z1g9niVGCHpvJbRTRDvdF-F(#sc%KCV|CPl;RHq!{`GzLb7T|W1`{u_;G51rwBr%FtmGt9pB7?0UjrV5tCaRURY!bpDgNaC$kk;i}|l zzu3(-pF^@FwAp(OEA?&P>o&Kp566fP_J$L;h%{{r^q1I!Uo^>r%XDnE?lf`ty~ zo6XUaGFV(thc;Z4i{m-xH7ZcEb+OxSRM$A~Y2zp>8-n(}Dfa^v`dT)ww>e%&j2|9f zJ_CYqVJK*o(o^IG(wnmV$jZ@N3CpLg@$qjV?S2y83QmL)$TKRfh?KaYcy4?@@ z5YriNsDOzHF>Lv<)S%MZfw7Xt@7m*9ydAgxYpBR~4h4dmQ;B}ZZm_Rr_Lyn+r<1QYJ)6l@1{5?AfpVkd z7YQwD%02k1;b>z2c?_-9*JDYCo#S6tpjETk;{Lf&j;ptTn8n=7vQ@0jJwQGv6M8!V z9yvU$fv-j6jYcW)uZu6cG>d^qerYmfgOC1L_}HL5B`pj(`#?v9KLwP<6P@qT4kL>W z^+~TX5bPBb5l{}FX$IrW%y7Q1cg6GNm3XTkLUfJ0N-0T5^Qj8rl8m0Ty?ynU-X?ZF zc>lOG7Imd@p;um0j}XS(p%(CY=~t@L)UKF|VEv zra;=K99l7SL8ZY^%7Y+*jo;pbxk#56p=Fa@i!Pvw_QM^C;_WJ{*<8j31`)^hcF~{H zr@ZS?csa}(uA~(5nox|G5pW;Uw}157=H^_5Rx5z~ST{7GqZh4xS*lA2VH%MfaU=O# zaJzq5rHZ9*_dpmvS>#r`F=9;g77s36fgR8r-SGp?Ss-b9aP=2Mj+N&Uf*Tw)Wr+}q zE92?F*p}fCg%m9U=7;3f{IxIdRUGK=xih3H#F;59igWo;Rha!)hSnXuKa||t%-uPp zO_?#3j{j7Zdlnn)b(l|0p~^W#r%eMOwB63@THT}e+n#hTnL>cOX~kTMX*0nu2btP7 zmthChGzZD6hG@<~&FDZXYLbd26%_(Zjkgfc1fVzd zA`L~y=ErxQJ6IhO))@NUd~4gU$9(J$Cc4@d6Z8W5`I#hQrq{e_LV<%< zRua@L5}@2G8MZuv&LKy_ce)z*e+9H~yJMR$#ykd;tBW|h{^m~I{{+pSy4Ctn;;?i! zTPN9ykUj+@J5sET+%J$a*y_PxF!3oK4P{d|?vM~4m@QezM5q4MYov|WPQRyKM$Hz#a@^7pSO6$MDST8^i*~yAAWiQs2wvMNwR?A`eqSsY!Gz0HTSfX|UDX5|-!{)Y_!6#$i?+w6Kk7uYz=?V%dvbM`larMMEIQoj|N zuS8;YA{2MStT9f-mCyXshQ7sO%v@{g3jng zB+3Y^VEB_JPe-W6D?)|NTdg}A`WGn1u$Qc!$S4J4*>rl`@V79byx z%GVFk56dDrk-;p> zo@R+Fz%+Q74M48os%^T#dtNY|BI$g2joP0+ZM*L2Z?!Wysenn>zc+oTIX$wM2g6-z zk9T#&Dn^ZX+j~mXQYxk{JgL6*w?$KHZ>h{zHwvSu|-+K36_F25r zO!hv6ob?E$wNwZ)X1eCn0uc5O>u&~vkFE+*!Q6nMen{rJVFfuHIMHZWwl8zt|kHcnP$QEg6>eQ$GzqOYo4@ zY!KYK^VA{gq8k&73_CBkveg2!1cm6hz6uPEH~yh8F=Te~c^FjP0nhcI!f8TdvA^rE zzd?B8lDZ&&2YWuQ6p-mFagx(*4D8(ZO>5IN;XI;}gH6C$N#bh9E{RHUD8A?K*-kxx zI^9b|S-3d2cb_NvzT$bAl@8fQPY^` zR)>aZ$pXoAMGr48!<1-ZQD0>ha9oo|^mHCM7#pENXFn=bXW`DDBw1d+5t$&PFtVA* z$d_GA5-?ri-kB&xa20d19>Qc*p#-&AXVLg)KJT!V9N3!$UMkL2;^Jtue^3a6ZF!&o zgB+hidnz;i0QoJTN?RNzC6$Ll9qr>o@p@mvtov!k`W$Ygwr|lf5|-v`5cUmQIV!du z)l+uBv#wRL`ozc*wis!dBTz@7JybAx0zSA-G|$keS42J!MqkZaKa6-x-ec9Q zsqKrB2>l4nLJg}rMbQJNl)S0-_;bjllu|hWl1?t~2Oo})-)*W`wu}`lz<&Q%%8e<2 zQ>>tJac8NkI41PZm4tpOOS9W$RG8Gk>pCIuP(7YMH|;ZbUbL5G9`am;SE|1Tmr@Xi zwmd2kbtQGmU1gTBk1oeQ5e1d~v|a9e<>i?k^1cAr`>bA`Q-8Aq1VtAd)<5p&Pk*sr z4cD{AG}pk~IssNItJonyK(ej7BKad9okAF;1mWt|$XJ1r@<7?ujkgMrNVmvUvv#0< zv5U5oV%Ji{r2vZwecYt$yRPl)@hcPIU8XZjDzrCGzTXN=B6rY8OBti6*&|q1gz9F; zuxA753))S%wNB+R4bR4(7OeiC54Xp9^$2w7yZ1U?Bs(J5vhWO|K0_DR;Xe7H~ z>{-#If<8Fsy-)W~$o2+M*%3*!21E?CPEOma-#_^j9Cu)iI_j!lqLU~CYKNVtHF(lh zdQIz-(VT^z#ZPK+u)7ytDUSCz!y~lU>NKqq20Yn(+YAre0fDa+mFXul%OGS9CRq9& z5+eU1%{A($U)#vD!);E!IG&j01SnKhY_XF~W;>sky!UC?QX6L;jRLcnj$|k_9&9Mq%{&1Ee4l#STJ3R??8v^0$v;(lqYqO3q=Zfe3+KDgo=rB9s?0c@H&Za&<;|Z zGqu}%pHyhvQ9ib^3DbPitRIyXL702G{Hdcc+i9L&3FKnd^RyLX(fZ*$yl`iHE@YL+;XzUnXlnsGJ6SXu zj;bMP1;B%Qf9+_=)7cDx{9yG#Af$enaG%hN99hFAc>m^82(d#Zgx{t&O&}Q3P~gr? zaB%>6rUI5XhNB+7p>PPqk)ZCTgLhH2TYZ;=-JNK0NEY(D9#lh=T0bTU6(2k?{n5;?!_f&$@kn`2k&0TW(@%h z?aUt;LZiQijV+>)cf<;Hjd4^9n+i<@+e9JF+TRU*P6Hyb+@C8JU2aI!Md&N4#|1I{ z5*~(Sdu#5XHjns24lTDTOZmL2hnf>9`9Ay(cb6G9ksMR63=z!QbPc}ZBG!kyTihNG z8?a=iaRW}^7cFeFnk-XtY(uG4k@W&LvK!N~4jS2&qqkU^p+H%`k*Q1Q2Eu(S$>sz( zcvb1S-pbHkLBVv94eAqsr{Hl3MT!L^w0-de4$Xvy&J?H=N-MsO(lc3d^xc>BN{eJ? zSQdB+dK@mgNA0ETJncT0Er*al{4;xQotY#Ng9wPg$~2{p+AGTtDp_RrM}&jJ&?CnCD_a+28>%9cAc z_fwL*66TU;!eUFyDle(hjA`W)jA zR^8iz`5_V*AV!7YQ;1TM6$1pup8I0C`92*}x>azgyXP>l6NDqt@jthL=xa|fx0-y# zrkb~>l2B2eE19g!UqpteGGIBv48>D5c4StVYM%CC{w_bv4yMsP80Uud6NybblqGx~ zYOb1b@dCuAI&%ycEW7dDDf(+^STVs%V=+`;&iy>Dv;zVb-`6Y9dO%xH2F%XesVzX! zo&q#B$x_2+mmT#6#sM3}K}5X8etH5_jStOb*#_a;_3Fw;J9*k6ZkYr5QNSa4m`_k} zAKvujm;*BiW3bB8gYgsf`vr+8)1B2e?Ju`=c$xTL9!VwRmnRERf>^wh4({sTGv^uX zn^dnu1h~D7qzS-Zn?m=tAo?d7D z!k}9GDP3e4v$)HYwI`>N)6gey)W6fXN$AS7c9ccBbq70~G>UIPJ+dk1badz5HEPO8 zp{3lDUj*^5;FU?4;_F}yF*nycBV=0v@uUCD`%J|`7)2lV_|Q-q_5v)+#PN?Xhihz@ zgWu^_A)d%#Qiu_SVun6vqN&Ve;I7)rf4@5*?1D+|Xc1lz;Dr9CP9Vz{98Gwge|c3y zwoIryzG1D6ipr|GDrOPy!81ArKDFT1bB>XClNmkLCiz_%ODZhTvtNA2KWM@ZNP(O_ixTtmENU~CpZXn#8jn}ys_6yI% zF7R1WbO(W;DLQ{!Q3VhMm-&}bLn;_o!rwds>uwT2#ANR-ih6rTfnquqmH>Cs2sTTO z6_Q0=$*vRL5*6CnL~O!Uu~_P{_FA7$VyFCr8iWzu$JI>sliuRyce){34LQDE!=-^l0pV=kf$JxhGd_H zvh?}yE%``tuD>L0!#PMdwTc>ur*fk!!x-^T)`7vSxn<<w0J5Zu)K$GWVgk2d?Qe}hg03`zdqVa}DL)3)asiTb zF}Nv*5?!$XvcB@FH-W)xF5Qdz>yR%Hhn@Gs2;1kVRH?iyX~Eir)-TYOCzVewhi5&8 zv&+nX)UE|Ixda_o(CC7w-jjMSfs|G1g5I-_D!>Ydo*%|)-l4v9s2Z^I*{n#2YFj>} zCTyU9r4vW`QqO835ULdO^%O^ft|BX+VdQ{vuW?#xCF1I$bSZvYKSU!5@yYqsmOS_` z&I(!iACPX3>xTXzQ-VlSOmyQr=lO$o9AN{>9-1QyL!m9F)At`Qcsg5) z(c*%*Hrj*mbcYiz8;P#@x}v>)m?}Y9p^@4^80 zIFEl!Zu?c}YzT8#+af5=_7tatLG2y$>KO$JvonWgecuahq#xML2qP1n+e;ni~l^@;ZJv zqPWf6*~FaH*(kI|gPDas$T;R1^_#KiHvVNWNcT_-9Xh(g9`CTKtut9!HfMc>9z@tKLu9%PVqQt zdRTz?TJ^`}UL&xf4x3o7g+%d#%@(mfTF&Uzozk)oQ2ebz37ukpFYNZYUaCzhXAB7q zD%v9^(U&PhCaq1Tr;J6NyaF4BKvs zySU3Rz7|t!;&B`!{1j#XkPnl@Br)B=ydGQa{2zPC z2(;rFzW0(QN|9O{W=Bmbr%!{1c=cv&&smZ6LO% znYklMst$ZtEqg(1j4VoyI6=L0iY4#}r^wc_Vho`aIUQ9erES>y$O=YqA|pNU^32eA z8gDRX<-}e)ki!3~sN$OYz9~p0n!?P5Ue(-l^uBVqkZ-+0o3say!Rx+CZ*L8ylm{1f zf^5|Zu7rO%)fH`vc{*yy#ExtBSj^7yTk~pX|D~jX<%NkBhBN@cchp>zWbKm%LouxmG zbi$pq&Xh3q*F0|KuufTZ;PE}EJ^T}Rc$HcYJ4p|Sl5|C9Y$q#j&(3BTOiU$ zU}r0YK5lbCWUc)*r1Ae)Q=>O$^AGA*1LHFe-7G>b;~~mL)hm=GcNlXS@r<(6@<-B; ze$+xD6o_p4k6eEaEx9V%%$h=0dnO1@pq35zejzAOkZ!`B<+9OWj_$7dB%11irSp=v zZU+aCUXp(eG(Y=sVTKMGWXbTq1?oe&ZGcw1D)TcHu~4iGPo7%$u6*pXoWF*{ZEm4( zS{1R%MzT$wF*SldA0)~GXb^D)KU+%ht~cF5sHc{7-T)rFJYpdbP1tTqlpsU5Iw)p! zO`Z91xd9E+9^qSUVJ`-~(WV2e3PqQ^s%N$#D5}~Z_@ctTkU;W~5Md(j8n)ixlKnB%lX!Bbixz88 zsvjEe^@@IK;(q|Q_69wMj61i*6x@WJrbEZIgjj;{k_KN(uj7H%k=5hve_~^fOtZ$I zJ~hL2C*a;r?+e`0U-qQ@8LljF{$Y+^b674m&`QHJy1 zM8?^^sDasXI{@~wHzAYGVj$uicw+6^zZrRpFj78@VHJ!V-W$k^my?W-R8S_nwZuq7 zMJ$!S-U+2}H4+O4ukV*PgUt6L0~EGjSB1qJslu%<%Qx`QqrSl`hsyjdV!#v|$<;xv zwJ_oqQw?^LMJ_5p$|bQ+)IX%DcMY5^5Fr;d#TqsE5XGbE3)|D`fmSCcz%`rpD>+x` zrFi@jQ=m}H(tJ_n(yy3V>cVm|qub-($6KTlvcAH3KK^zgq;Km9pudEXXH1t%Y)EC< zJI7L_Pf6Alrwgf}g9m`ZyAso)jXmtq?`efl1)&q?B9q2tP`MS6O^~Y;?noQ9Gf~;y z$GSw_1O-716HPTt_@{yIcxP@(%gYr&e~MGOVhYRU1NWlWaPa@rcXrK7wVL)IJ8_Ua z1DEGAOv_wXN=a4?UEjU&{~mr=$-uZ`A!&!}MId>2tPrE{=H*ti!zwGarwT2MwBp6I>F zN$#D|-~5RsN~b5~b-QcJ9f#?AVF`&!!G`z3w^X@b!E{O+N*`iSYpb$Gk^-$UF8>#h zVPYdTfDslZgMGtTvl1He%>D=;#%ZL6S9BvcGgXs3l(>tc=tbQ{i&jN=L+AVOY-CT! z&SQSXsSc5Xe-{GrI~PW*-e#B!DdU))&V>n2WYIyH#Pp7beZZVfnw30$s4)}6fpPNO z4F-fteWUdUky@-C>vr`34ti;*BVZa2U7#}b!nvA2b%9(A16l63MQAw<<-Yl&emmUH1cTg#KubhD? zXT_a!#*W|X(De%0Jd6cXSC1)}5ME#*^B01V7mKR=SQ8fMCVpG31fytEzQu+{=G{$w zw^$MADc&Cg%NU^Kv_Nghh4-%!RcH=5hD+O~eXfuKNAQm{JB(Lxz%hcoMK6R`@0<>`E^j_$o+G`OMM@cf@lb+0rC0K*HDG!fOE~u$y?Pz zkG}G&Ckg$J@7(?b=AagWToXW zq(IupH5gJnd+uQ=!qfN9$e|ccmWaDP(qYMz&2;jclK1X{@(w*@Ow(WYZW0T1Of4CQ zZ7Tr%BQ!_N-J4aM>1C;KU@0`%+Zlfa#dG>`C=a+FSS34TT}cZ9?!I*%JHZX~agPo3 z0BD|XS5uV&-0e6!cbzo6vbe;gn;xWvNc56-UhjO98#O2O_AOEha_-*m>v+Uwe|gt6 z4ca!f^TKh2*!|aEeisU=fta~rFzUVhh!35gC73E9iuY@$GrTe^MdelD9SIIwSo3uB zT%;e6aKpNG&7>i1Pm;w@;C}=#O_$0cASJHq%nrJx!fSjIW7L%*iSYgD=5HU6*NVQS zs)6=M>+Tn?ecz>PnP6N(_>HSLc3zoEM61jKP4EPjg^r5$triphSEd%$VR0l1-5j+k zg|2pgA#<)q&j_AQ?g`|fxELR@cHZaPh)K6Ab16i`SI_R}6+a6zF8=7cd039v0E?9$ z65|>9FW407kY^=RCH{FG9mk5r*}dONwL9wNq`FvIC2nrq&s>jJ4?xb=yWxslSz@O2 zM7ZHI+@uUv37#pCD80luIm<)9bxedJzEDFDC;>aVGW=IP?>!7*H7*_WO zNv3iF4Q@rTvDPdettu5HKSDt7=J+WRkWU0@9p)IgZN}ct5S!UU^_7U|@aYg87$RFQ z+EHaC%sWj9Rg#1!kdg^XPhp=Y0FKRU>J5?8h4tsw9umay2NHH9M$%yJ5!9?yOcpwM zZ(ZJ3U2_>kFqAm{p@BUZA2`rXP1m3~pcqRw$$~}X55{B8pEdEja?e1xkqYYYH&y*0 zL^;w}TaIxIv8XE|tVjsQBqA=;xl<{7s!oyK0XGE1g2$$#(IL!@`6jDZepMh6BsQ)< zR&0Z~XWmM7h?RgkCE5+eKXvW|Z*&dU*R`j1#`E}o2^@-gdo?xa>tloX#3e-bLm!5+ME|M;XMzPf>?W8{ObitH#FY;I_jtQXbzGyK(L8`hUK% zE5evY%4MKc9r4*^LCz^*G8&5CPQr7L3z!rm*G<@>TTlOF1Zg=YWU+<1t+Y--_>Pki zJb$1#ttCNOV&HqKSJBwLZ-O-)t?Wdp_skH7IJ(l~;pEm|klR zn3@76(!DxFw%>`rHCQUkvL*2-ODUAYe+6GinL8?7Uiw9;5UHW7cPTao|CQ3U>LR5* zBNMgU^r{?MVCg`N#?5g`@=~auwKKgpi!wwHb=Ic!t_D7mx4zfN1pl$=lha{K;uKPe zCReGPQNT{pi{1aMx@u=AtlE9F1>Et9pP07k-@d@AQJRK(Z>Li0UY0MPZdwPp;Z!1c z=`F+WDm9?+{BU(Iz5aqhGY2F)!^~Cgo4TeIMgC6!DIV70=QDl`w)JIF(bnsuWqRs- zJt^!!e>;IDtY%u7+S*Ng-6%p;kBBp)K!6%W*v}y zjhFyrn#B}3nG$kQzQkcAFoVQabU&iUYDeBQvFy$aO1Mtej=-nkCzZ{^{BBHU8K`}6R-cr;JS zu5)6sZnb;W*X=$iP@>(GHcSd@E3b8;!e^6FYF3rGsoP~XHDxC2V$~i@%qbq^)5n@7 z(OT+xb_<8MmiEixQlN{Cilw5}iGFeVY6q(Sm+H!ti-#1&A$~8wY-7{_iz_8J)oOs| zCvOmM!=PbzaluNONmIDZyfB{3iv;&5c1wM^EBI#V7Ck0C>Vlk$T&J2u^D$6kMRQg7 z-mpKo(IGF|3ufsW7p>tEEd6{dd7%W zObxW+ryPDe0^*F89@`%h(q>Pg{61J++-%5+qvA&3n5ff}A8;Y$YUMk`d%L()`cdE} zuu^B!kkdSnpRj!cOHt^o(*Xu_;VG%1WRc8>s=R<*^1~vR{(pON*iej`EsUdCTQ$DE z>!^L$-&f5v_`SO{-8D(h#`5Rhm+oeoNJ0=3TApi#c(N$*C|wh*tOl>SA6S5y)M}3e zgixf~2{ZqHp|#U@(&VJWvrc`4whTa(=7pr7`XS>DK<7=iXBndo44xLmKi`be;k48m z4uikEc?^+LRP9UJyGd<0`uU*jt(2De@*!g+&;-+6aEnwt7OOpqU>E|aw6NKiBDw?r;1So!{w&z{kX@f z(99Y$o8`jP?wQ84{Zh)KV;eM6GHsB}8xR-wfNxjS3#E~wSAaO*nbVBQIJkDh<3V|STxHVu)aM?j^MqHLt^{;2Av&^XwEH@2Z9qbQOO)76wRnMIlm z?9?t>RAJu*OM!=pR%{&``_*dcPdWdpBKlxy&nuh0mUc% zO%qW(&(vuJxE5`yYpi4sYmFs}p4RKwOKZ@Y?GMwus(?0-dKb59#uvV^z?QHB>7aJH zpS1^dy|D;CYW@9C*G^*V;WmxtDm_D2IgLQyjMzu^_vlxM-|w7-aiJ5HGrDhceY+DV zvsLaI?T~Y8eDGthpE&1khO8j2QNHhDF{WW+GFW&e7n9M)h0;jV?7u5J+8IebjZ%|Q zTvHt_!#rh?eNLhOBk@7ea|cs#6aN6gA(`~#c#VO<=7Z`ETxLu_z%RY^4-y>Cne1X2 zTD&Ar+s`M!zzm7skPO9!D1VRceS%n^Nm3SP_E*9#yrkSc#3sVvZXPH7Qhksr4K8(q zgwNn4Due!N{UAi?TuPRrd-K-;p6D-0!!a-;0N-Ow(^@#j5)OIeaU=QpMF+{l>*~~i zGhysezXr@_9VK527VmNFJ!~(`4NvN@Il-{UURYHK{KWm zFGMdFBo=Q{9IYtyn@>;OvSB}-I9n`M%6`|^e&WcnCqdIAB5!{TpAQohyhG9x6}&a4 z=2mOLTyIOt7GH3x+Xfwh)vbVfHd$O^4jYw)sC-7fu)HxQvWs~4r$E<&jWDV=T))$D zKC2>ZWi@Dg>Cmsh&NNZj!lRNTf%FCMLY_)|-(ljS!_g12#~ZxCxO;rLXe(Arj&I}`k z!h`_U=%(DP?!NZrwOS8`hH{vCCDfZP%n*_KRl9aV?d&sBt( zy1hZ+xKFnwJ(a*ZzsicIDuAtLBDu~4=A#TcR{H$ITijy)cdG+}dK%T>s@^BtnJub! z)^C_5E|jHkCj^r89Kd?OXnC-{TSQMgxX4@;=e^Xqz0x=VnE)UAw7c>R->eO+=NAp1Z;h9BBJ$-l9N2wr$+*uyy9ot$0A+ z3+|kg|MohrZZivMtWpi)5$LbyMXiV3`-3N-eYF3bb@0(mU4}UNK0x*Z6HkoK)@it} zl$(qs>HB^ZyBV7zsu>z7=YvjPNN8@KzVok_acA}*=pD(YZmwTO+J%N1!KcGMv_Uz; zMk6Q_wIQaW^{#2r(lhG3VvM`IW4mo1XAt)Mn)evt)eZ42l7QBo&YNBnn@gleeYd$? zh6!pri<;Q1D_t&EXkj$@x2!-rz)Q!}BP8qh1X!$zYv&HH=L|VxGL?mY=~;(#$V|@5 z+YZjS`aRhG8jfCk?B%;`0Y=mscvNTi7T9VZ8^ZkKb!P)Ps_yCV?^6QnsUbQt>ze_H z8Hmnb%uHf?N$;~jG>5mcrzzOa~TRICqhAS)xML&`jJC{%v^!$(!LmGgZmOg&Wq*7S`m zCaS}WglnYr<+PG4?j5KMk&j;E`dqEfHsxq4x}? zhYdWvY#;Y4!s^gwl=pq#?||*!o~P;B%eC~1{1vqzIm80o9Ukjw-)&ATxP}e3imp^B z(uo_ggTD%JR*YD{xK`s4OEGHdY7z6nXgyzB2cvgqWa?$dldlzfQD4bUX@&>?{6WXH zn~xcBz)&E}l%LNn+#x_7bf}ZU1QnNb;0qgn`umc45919XmnHXPi;f!ocp7IF(3}ep z+`O*3JAks3D0WN)xjPUdfy=A*um}|pH6>Q-)%1})&fx|X%WbyaGFwMU!uyT>7dWNV zel#pzdoos~u>Z-NU0w_*HR}RKS8~Iio8i?#IT}v2DVgYaHDAl_#`}FxcW9`A z=C#$o7sFThfRT`UilDnd&F_w9wM1t#FplF^Btob8)(sf5K_MX%~B?7Tzz#x_-*4qav7wzdMc_iSu+ zD*IsSKDnWzr_zy9#wKZFueLrM=6zWS&Pjuopt7+u$F90#*!5(^3nT@wZ4&h#5>%pg zfa~J7PC~Tl8Dj{KIUf?KR^Rug`pCZ0^E@G)nUEFc+@epfIvsQ`-tiuNSL-8l!fw?u zr(>o!FfD$jOLRV?s$izNP`*y5lq#fVe7kh8t#!JltC8EechKil**YZ%J=~AB#^Bxi z971cZaLog73akQSI-RM%l<+crOz6hMH;(hLF$|m|MH%nSSd41#6DLi{yx-@moyU%3 zuRwLy-8K-(^(SgndijIG08Eb`fIV?z_8yH@f}l}shN-Z7b8u(8w0LNf{|B`x2TA$N zU)nptIG%R=NE%a8{g>6P2-gJmYowjWi_8{5br%&? zsxEbCe{rB-&B(ZK4>6PU0EYpB3G{ zK%}YUSv!S24{_B=&sZi<68P63Z$sAZJoBU@EMn>D$UK5DmBVq=6Ok>VuUs^t*<6H; z9Nyq~6R__Ub&&?pzEUdfMgM~qNi0AdtI`)Oi^Wc5o4&Qsej}((4Wu{Y;;gnIW(xwt z?%9?X*N?{-7R>{=&SWKYp1av>po20C%_MXEEQ)3-&?WP6(_NOi24hiG=TZGTt8)(a zC#Kj)Obd#Sd*F12m53goouCnK?G%Dk33?{~ita^b!Qd@BAZ6ax-Nu9ywlwC}_3Z%S z@^M%s0qm)9Pll9}yXrhm*tM`d^_kIlY~gA2S4E>$>NE{|$T-V0TkLQ{I}c)42P#3f_z>dkNq3T62dRn%|L}wb z{Ip+T;40nyg=dUgw_-Q})WnT0To6^T8Lz;?fR-^lz6|3d5xm@h7x2ZZADrplnKdh` z76>w*EEd6+AC&;%{Yi|h%f*@n_lo$$TM^YPK&Pvf+2#v(6bxMcS;HFfZiu*d``@OZ}6MsW1?h*7xK@a}h9M|!@K z+SNAA4pQ{p400_-u^Qy?NKfVtv~GVc8Qkh4M*DG(RV}tmJjcG`T+niz23G=`k0bT~ zQbkACMSt4>oAIMT&@62ol9vLQzi>l2ox^_zUyNv^svgjv3(Ir~9s0mjoH1cva}@Kx zxia8(E=if^!E4IxRK0bv3p$^=3t`PbdCK~)JljR1cw-QA+qGG@@d3{)yrkcy91;4e zZ1v;K&CrjiGBJUZ`|A+$n74GF9=}legb`Pu zjS)HZD|$;+KRctbGIh2Sh6WA?e+cixWLgv%^ACSr1ybpbH9CO}#>5QytKa z7DLktGuS8c%O7h)Ow`MetTR~l(ByI%%@@QI{N@Rz*kmDNOfRv(zCDBZ|4C}d0U?rNuQf@k5o1VXtOrEq~$(d`Hl4;l2g&!}`rkfauFvi#@W=9Xxn}6j}r!d9oPW z12qJeZXn{bDZw#WKsQ0XU(gaUWu!H;+(X!|A|+aYS$@(nlxr!znLgEf-Aj1dry5Rt z`|ugm+O6!`7sLO75oXj%@~59<;un!F?Y}C`1sQ`E3rLmApShwj^9?vPX9-u-m>{9y z(0$`#ZYsW1O+F#6)ZDb~qp!y8kX&6Jf zC*ki86G^_#LwKIs;ADa9nkU)&%rSN7UY>WsOU3vn)f7bF~ zrrtB<#U=tchR-R=ce3+!C^6HqoOdOS;L=a*YXMbS6q|fOglMsgc}=p`4>d1Me?!7J zUvn8if9gI5odDn+nhyhciy7`2#+I^n9E;421t9q47pFkHNG~OfUQA~#*zOszs{VMT{9K1<<$kb-KD1=vZ9dGY87imQ8z`qm$ zwO!v*O8l?=`PWCn;8&R1LqwXTVw=8P1Xxn=Plp*nLdke`lkzFLrX-1xRO1f+ zd;;)3B_$mOj_l+C5W?<!MOuFg4H8gJ@lYEfuL{#GYUQTZ97${(H#isMTZherHmk z?fxhL?ir?l48zk{CHs9Y1J;YgdO##%d5$RifiXa1E3cv3B91T4G7QI53A7zjAB(kb zlt9UREmorUBt+JTv}LQGTRr|Kr`I~wK0l+e8DG7AEReOACxH%`foJgi?n8nw^@)B& zAhP9iFE~Qt4Ks};iHo7p)^-heb>27cB4^dbZd{v8F)DlbH}p6He)nv@kk0O3TzC7l}$WckSr{5tPG&JRuQ9<+% z2kW}=zeTasNv6Nj?(B!Y&whJT*7=W4j#MS{N^@&SPR{~=90^HV_ls!Xj&!9TYv0d- zoQxXTBxYFO&*;?!(cx%Cs+$)&ifl<5B!t-QzP5uX+}veu-3A{}ps&)yfi? zQm))Fvt^FK_q!EjNr+^V(0K0n=&_sKhBedpX-G&ZP2J^@Z+w#JZxj)GrUL}Iyvf9Ufak^D$^~pz8W{+S{~T_`L;MV zVBBLr-04g&o0+ZxPbb~yN9Np%Z+8hTVYJ9yZc8;Pk}7&WvrB8cSO=2}nY5jp21MJ& zwWS1Bk&mDOnb&NU#B9Pr6Zc*cm(hT9+V5yTxgY1wO8nEIleeGA4s8c6`qoLr5%crF zl4I7Dity>I8D@NM%ue>7LilYZWW0BmR^kQGk#6u|lwpHa@E3z?&46c|`2Mt-j9M?s7TV zP4@h*zF&EL9HT2nIdtP7;2spCxW+(f%c;NjG9}^^LY&buOuXLa6Xm0;boVO@?x8-N zM1V@rF7aZrTUERXk;=#Y6w5&)ckW#wD$(CDBVWp@8%ZuxS+)zvb^S_QKHc30#VSVD zyz5j%KI&SgPm`BFjCJ`^uT6+6zlT<0;z{-UBp%p`)GkkKP|P=CnxTG*6i{>Mgq_-y zsqM5lY9o``V>m4>ESr0!8y?|+Mme&6s03^T_)GBbgzpWII6$?R`~ic97?d(}Xu9YB zUV7_tuKikJ^XWsFodrxE1;hOqa4s zYRnu?F%g)zs^X-?>(I`$mccK8f9ZnHGCU>d-ZayEhkU*;xGTLL=T!>3>HklQVh}g% z*7}qSMa{ePDX%-qxU%Kh10vm*NbFWYUEj{>^yB z#Vukk!8+?X2N>9UhcA&uo{F_A>$%<7Km=Q{nh&TBAeYtV&!tO8ti$$=!GBf!iKqYy zo4kzD2EanSW&gU)fj)Piw$$kwe=Jx5@R3pu_H(^B-qS0sWuY zUPmVn>-|G92SS19HG>5+jdS45x;3tpMq=aK&+hi-)$^FiRp&@Z3n6gY3?9@uO1u6B zed!|Jhs2lEwi?si7i?@(Cg%7iD)6ZO$-!Cz>!o)8lssNr6f zh|QJM63yKoZAQE7JV4-5`yNB`V7)Lf&12pCQX1Z9tw3E7tTBHEwA1i6B_*ox_B8)X zZA?wEfR8j>{y6Rr*LpU_>cOg5F;7jqJ`dSNRf96^%QHjBKjL*eBj+<^S21OTAf zN_uTmr2S2bp!kiGCiU1l&8&I$o^0E;?cJko+cw7bOr5vhAGFQ8MFay$k_>g9eT%{B z0m55e|MmK>*MGhK>-Ar+|9bt`e}Co2i5$%&gS{;yx zG?Nu^x?mVp3id63VEWKquX(~mDgH2c3nERva7s0Fo`Eg9lS$&N z@9#vN$sbO#a(NPu_Z<4{;CXL7gD?tyOi;C|fOniVzmq55`a60&&le@Rg0AVwo-6*M zGXEBc6xR||+0X={AGxA?|rA&Uj?JNe7!cAac}JJ`|=hK zU-*?*HA%|&T%NJHb{q z;OM+IOmo$WNOrqJDWvh{zmt7lDkY(RWjlkz&(G;)LHNFprS;=dFOz~Vr zD@0r;&xA$yj5Y<2)0}RoRHQfbLTJwuW5A_h0N19|+tt65yZ&BjAU?jjFNn{1NB2aDMK{(}8(<%Au z4*+PIgP%nsguv?@M|4c!RmkqxZogtB(*=4wLyTd)=Ch#$_-@iW0Nz{x{kK*B&-4YLkp?7XmLtEVb6UpunQZ6fdeO z9dE;lL^3<)KzPx6=Ac{~&2vV{ApLy^yc1(PtQf*ks!&h_F%B%2(_srvdi+lJ-{jAU zrJ~ICTPE~etrEKvx;K>LC6Uj@Vaq118o!`*Zp+2`olumjA$NPwt2Gse|D)O|S&{{T z+G)4s#SWKFb>kEyb84x|#o8wHbe$>t|F|zf33*+~Bw9{oPwc?v!|oa)BbAfHNqnLQ zf%jt}pXc+{Y}9cp7`yn^U)<6HE3ecPA=ZY#EAob#TVgPM)cL043O=Jv(Y1K94}qs- z%soG_OeCOSK1-Dw}nD@rctzywF zcmuw-)mIt3GDq%A{XtPnfb~25WL)K2XD~Qe7N&%k!&BRhakBI_wgQI?#y@H z2;O`t$pdZ}(_L4v!<}0JuBJO5Uyhg1Q+bIF-v{lv8Dp41LtT#e8r^jTeAC{k zDV_@TAcHskSyOHmQx9U5lxxJ;^JDkmop>ALz8LVLoAj+XcX?o2l@-@GW^^e1@3 z_!kO7?mD&PUA0X{Jni9;eGArooc;2;A8rr=M;&$;L1|Mu%+oPTqiV~Ku0DLlGR z=gpY0{2YIQjgx>j^PG`f98~qn*D&tu*ch$+$mf6lS2g}0>bKh;qwLd@eIxi!?|-%5 z3@dz7yEhp3;uK95A_f3AM^jr{`&mDf2IG? z{%_pJ_aF6NvLE99K);>;X#d;okN;c$x4IAcPy1i}y~Dqt|ET}d>!JI9@R9v%{rCSb z_CNDK|NRhuMgJ%NxAy1%x5x+oFKb`mANPGgJ5j&c|8M+v_g(bA<^RflUiv5b&)PqN z{?-0d{m1x^+RwkQ-@noFRq%h=f3p8Y>W}g-wttoW_xqv#OX+v^U+X`|{zCe{`#-i+I#_iMf~&pXZH`>ukoKe@8;5v`QPZhfqk3$m;0~p|LFf{K1F`#{tw!3!{569 zh5uLnC-M{Q2l^-U@AY5aKYTyReEt7#|0j~~;J@Jh(tHAcHU8263;e(OpV{xJ5AVP9 z|DXR+`-$$)>CgPX=6~1xhkrEw)Bh9vC;C6y-~azy|NZ^X{^)=E|DEu|`%nM4-(MHc zwrh9%jwJ)TJW@(8pKR9e`5a0Ic6g+eUq0Eb-|{$=4(#zsD87BOTfgLSC>`12l2Ls7 zX19OH;!r!Y#U!Hn_RVhpk;I^PXH4Ck{Ag9l|H^iBE!j=M213N-v*mW$i%5ZZGo6SS+I#HUFM-4;xAU>Mp7@i!q8DoSk^P zW$6y@4c^|oLI`DrHKXh5xYXAD9@hjAyC@>UCrfx&dJXc}S@z$>21T_UXtLWHU;zmF zRWAFXGF92qxMJ-5bIcQ$_7ge;WE9r1YB1O5G6u@rbcrGQqd12x{hHE84+Oe+K2PlO z>?>$@y*~^xjkrRoY@aJvk7nwW5r0^B=FOon0GLuYrTI-`Zh%B}&<@?b+CQ1tmzAZO z5_ArOOtBC?;cv|1;nex^eNSykw^vBvTc4>6VbkbxtM>Wnyc#y11^|1%`dg{6I|(F< zIObY8#$-PUID8+(E*)}z-^wgc(e?XeK0VG$X8Go5C5bd(Dn=YJ?^Xbbeu~)ezODcl zMJWq$&EhuB!Q_lbqq5l||C->56T0}i??+#4CyNVC2F4V<8yj;hP|Yk+K*Q!_$YnT) zf_sB_49941!S&N9^wqjuN~43RnYgfzjB0;(oiER0&ZIkKrGG}Xn*CMo5^PEsk~ra}|a2-r9n z=|>gF)ois&nwo0g_4d5ZvPyl8_81;U(HCAXqYY_J$;up?;@V8Fn&&K8ZC8+w4qVU z_OAN(=;m(uU5@MyT~dd3aB1oNkgOGURZv(6^8I{siJO0Doj|uutor)#5X{ngvKypv z=Vj({MN7?!2m>2|(awNH<}6c$7r@$DU5R5b=)Rn$I+)|z%X+x(`oDXYdMRY8mbYDB zgM1qImW{x4enb>C3M1CBT!)G5hOT0b%!PM3Q=MJal z{czsh_&O)bM&=YNn#Wm5tAc`bW!*rxi_5~G18O;0_$EKGw)V4U@EJ@V(pgch2@=PK zz*%JJIE&4>*^aakuPxuFGE0a+e$sBbz#CceDNuN=cKOtn0*;|Yo2356$H+L|dQGHlOie?6Rp$RLL3p7$)=y(B;X_If6e26;?^9qGu}8)kSi z2^vibtCmqp0%of~tJRYgL|IBMiz_5F78%~{FinMu$#CKiXG5%2b|5Wxb;WSJTfhdSwsy}_9dTEK;Rkuqc9lQihG;Q+ z@pTFnzzd)VfKa)^YRgBW|J&YrpW&rtNB*M7SS_11xcfw}g!ZDJFycsoyC^!f?_X`s zj2Q4jF4W~Xu0N|^6{a4a9{M|&B8?unwlno^v!#0U5nY!D5U(qX6U5MUYl`ZQMb@KoBkmA=P*Tcpr=>Dny!)c zN`7ndewF>hiuHZc0NQKppQ3d@AF_C7R9-d_YZSv(6y8(83CI;&qw6+)G)*N3#v+WJ z%Kb&^M@wX*-iL*wL46NE1?D=i1;;R+J1rWMhN_)dD7KMCU!?aGxW^CF*(HIUyD>OD zeCJ-YOA^PKV48{U*hsUbfo|2)NdUu6wQtbl`H-`o*;#9@n2j6Iize@o@rOX~Jw*bN zU!X~%QY*oM!;wMtT_#`YpkrE2QQ;Hcc|8o#HJZY=C^i1pi^xD7y8nilh@x=sebw;R zdyh9sLfG^%66tOt)%rgWWU~i5YVpTn8Np=6e8G&ZE5xc6acc1p2)i{#;6*e}?SmH5 z=SQ3eyA1Z0=hGzuUs>s0d;f9|aHRjm4Ip*Y+d6kx1?8yN+zkAbV z#Uy-sAN%gZZyA&>mBfrB1N|VTE7?Q0y*KkmEE6mHe{8Y}pUOgDBnNotgiDtAM~i2Q zfa>9HE`D`(dF7qbvyNho=H;-RK$o--wTOhs!?uxLIVkd}@podm?O_S4!0zsSswx?~ z#}q&m#NaWK<5Gw#_0-j(gVudYI5I^G#7&yX0`GeU@!A*O;#2Hr#;8lA`ppMVrVaoK z(do*HHGS#RD?KnuecTh+oNr zEG!b*mQGNyIYyiXQ&_U*eZd;HNx7cC^gYtAphwW2n6@b6sN1AD>{b7;-ix-{0U*xG z?ZE#1F61a`MG>KrTwMrc4;z6KD#wXJ1P z%2AdSO;z27YSi!X35EnybDq7r$~tud^{$N79b)L|SLK{lX(g^LS0_cA%H43Q@?qKn zHG;6lnK-_X`rw(kx^WW6@>~;Dmde|X(A9pr#z>&Dgq0Z4f~=Za{vzx#O({&;oJ}^G z)XZ{Wysh9-qAmprk=!1?2LaW_NiiJqJH1r=LIkJlAC03^(ag~lp+T?_DhR!tTgZz3=7F55nxETFKx?mDA&Yp9aln^qlv~;RP zQhyjF?wJ6%_UIJ$DNJits0xf=M_>g2LBLYdBiK$Q9*$whTEL6I&1_;Bo*zispoN1Af>mLRGnJ5C4hb(%*edP^4@|$8Lbq<+pb?MW2VY zWUxgX7SYCs2S&o6<$YrYFYOZ0VTb(1I&wP=T>XnVR;N92} z-Xg3Hw2T0UB_~87#obNvm+I!#U_oKqqF`>XXc=~VXf7ks{n{m{`QhmH4BlOk5o;K2 zOPvSVJ3X4#|J)H?o&rMMY!m<{pv$0Vk*jfrcaM2DlprmnViqMrr%L)sUXBVhxVGrz z3~sThOKZ7`L&md;L4SXEX|V$Bjf9M^{d?tpr@6xo=wV1vq+hD`ih z8Q+Z}ufq?oGhwm>DkU2dJ!ptuDT;X+dK}EUiN+cXoN-Ml+*95ZZmI+JhZf8Ns@mh3y|n{%B<3jA(IzJaDl zw|&-GwSuiUS7(yGio5oHEDrJUhk^m1&JJyPwGH(v)NjNEO5oO%J@+cS9B=C&7?u4C zL$!b^7LRO9$kp>X#s(LrxEq|J6qmz}MiaKN#RlX{4c&!Aa{)DsKzx2O zk#FAe@!dbO@*(J9C6?T_8m&{D4OYKx&LYzxF=gW?^K3bkb$>to9MAKeb23P?v~brGW(dvj48s(nfPd%hVq z7+gFMinr}~)yNx_dPh1#58d*=uZ@QnT&}Aa#7)7OaKn7_y+LL7Q?7e{dzwo5P7!O*JQV$;Qa|`q3iHf@58P<(xSr5dqF%0etg=sC6 zraJFzyLzCaYCCuyTubI%sz03PPazf)q0n8>b;YJpa;W}NC9&P?swu(r00n^oRqr6j zPo%*M!D_px_Uk}Nqg?9`T_{L?zs46m^GF$zt2I5Zf()Q04A0z>2w?uKcw)?TQy~TV zNu@5t65Nk+F&aVub(RwttLE9UuWt!Rqx?%3>=c_5+p;a!f$np*iq{de^ZwRq;=`}Of!q}HcZ&lEs*87nX#olNk6Z^KM9vtqGlUE zo6O2%5>|b|_N1DzsqD!)+xzK6N}Q#=;8wUS!o-<2ocr`972ytf{G0EkT8AIDFY)rt z^XNe_dgy`E&sEQf9?$ey2hK+q#QWoI`fU*6(xQ1chTCMXwh`sw)&P+dPjp;{uL8Xn zMRRYoRLx;dbUhB_2E1`wdd|R^U=Zs@^G$Y{#=&7gUTs2;(xDpr79Il+U%|92SNG#f z61^Tl>%*=gGpbx5gQ~=Dc~>NDxo>Ti zO0_iuNC7tHEooeBE`@@QXvJmOTzJdk-fQYWO|}~cTuV46^wtH9Z~^n(Vk*-ksPO~z z!!7XtJj|9s%^Iuntmlh&dYhoG`Y=9tO(cov^3haw=QJ@r(Ivqt#{pe+QDw47wY?>V z4qIH44e^42aZukCQ9`uBKr#sxxi|Rfz9$l`u6*jRH&sEr{0UC5fqHt` zufqV3sfu@!CPo9my9LZ)PtETF}t1>;nC}7#JHkfSx z$gJik>f@HGa4U**U~A^oH{7J_hy#_f4;r}Jh|!cJ5}7z9{U&!0U9Jo;;*?!+DL6rT zntI3Dpe0kqe^gKZDUuOAfPvC=^ge46@ZRm=|LXI}uDa#Lc!95u#Luw4qhzUl-R`#o zcsxvQgseDW5&@4jvA9Et<&6=H$7q7AV%1JlvK;zoyGQYDQtgfPcsVpk6sChx8REIEJdLc; z;{k0(v7DWvgVwQP)qYk=-bho<)&ifK>_^RV_=g4I_X4DM1=SI?6Y+&Dpx`YXr>}lS zQCG|s16sfXzQuVwcW#hm1&jsO%7jM1T8mMa3mAm>FfxmOu-e;tKo(fw27?$Ewu8G+tBe7NV~nlmHiwo^9W* zgQCUx%R&u-!;GPKwuDCM&J_X2#e^)O-=$uX{7${J{KtRJw9D`eKxO-Sbd)nt?rhOt z=`7Bt0Ptq6znk?k1}vsh%!BOv#7i|<#JF_f3hr0`Y4xy`%}XeknW*_a%&_sXa#%sY z^DzVWXfirzDEc4wVtE*8Ffa8u(1Y2J3G{?|a8QINV-JHroejZff;}3s>rT-=(^AA} z)zwI)zskQ^??X8OKVsA7hdXC$#*@ko!5UU@k>0_pr!<${u=hv*RaiSQn-B z+J2!srJR%{Ets^5yV=ihSGP33o#L6&*4>e&c z_VToj{yf-P$Oe1(-U#}6dn%MUDV5MFE*3ECnq`kLgO2R)h>ysk4w}vf+WCQ}z6gf3 z+^W@3RWjEu6d5PxKT%eMW0-|4F~Y!hZS=tF*z;5dUy4&~-cbXET{E|E@A2)O0Id=m6Na1$CZo;D zzKW{<<%-ho4d>wB^MhIJRL22Wm=SeTPYUOib4b_v-W+O)->&3MigDidUIhpM$U= zkVG&U)#q?DJBXskxNT^U2!)thEn%>sYhylM%=(r`T2KYe-%5!blHjCip(H^0#I|c? zyBz-!$m-jvPe3pn!;*?dv$GWS>^wti@_++CQ0eY2?5GvzbTfT z*OWt2Cy5TbRp;!Y`^-i9#_cLZEa9NdZcxaF1N?JixP{87ZYeCza0%)I{9IZIQ|R5RCa_u!X05ztl)9*=w?@&})uZ~eIQ_krC zd<^(mq!6F+G)xrV2%p1d2na-&6O`({vMM-q*Q;SA%Qgz|ZOvYIzV;ZAS?v^}kn8ho z%_P>$n8$98wHz)3UuOa51k%FkO4;~!KXKa+(@`F7x@y$MaJ+Q?$luOioxq-pXX1UP zvhNZ_mKk!(MkN=@XxF1`NA}|Bb6*tnD=N^(G6ZQ^BCH1i7mwXtFQC!?{9>Vq37SpL z=p^cgDAUVoC9qJRy6SQjj!^3SeeXu;nL=7%=dku#z zrZ~6%R{*VbVQEMu(Y!|CWScp4jYzAzJ{Zal%1h$+>?8tXvU18DZRV_STv3T_6rp@J zE!@{E@X+dM(P1qp(Oq6PmRKQ%ofc+h=MOWZNl+Sjl@8`plQMeeuPQ~>)4>gLuc!1) zrZ9gjb7p(;*(8Ez5$#tU|4w9pj*Y~4 z9{F_(IrPqW7i7`b2EW|}7-^wdwU2N!AXfuCy4~>QdRF+yTE;*1V&c)YE$j8_*=s@aWKRVZicxF1CKail4A0&{(E*b`V8;AnmymUM&rjV zjLR!-r7=32&DWd)6oE3)4xCsLB}2PD!%6-IR*5&Cx$5`)Z?Zoo>i>7-p?WSSB^yUB z?z&CV!PCFpyXcJ-d)T$nviG9?bTsH1z1hN4?TeY~IJ>*koa^8O4>B0W)_9S8dwal} zaOropk)NU}g2Z+)2@pv$?;D&~6fEOXMpk(xIJQ4&jnfKrVL6+AZ2l`qJy2zqu~56c zpj-|bxP7|y1txFsDQXhOuRxPCa3|c|p$}78ocV>as!hM+a&RwNQDnm2yI=@2O74O+ zZ>P>nt2eMie9=01e80+^o(!rg8ulM%`~zry2_ z+N#bV+s!FG8yfVF|8ngEHL|P{nOjA6sjZyUfl1PdOL(fFy{Vzg1^ z!+vPwou!`UBF%wvAmV~y6G^XXf6C+7&3a}s#|~RHC(`Pu+$<%34hF-Lj;d!^-x$A1 zd*$!6@+y$$V;caYj|uQ%+_bvi+JBRMd!#zJC;MmuDaAP~5qDJ7)EGPC(?R5{fA zeO9qHA+)g>Kj=#De~CXIDiZ2kfRFk%RyMmwHG&g-96A<&>kML;7OzI{up%aj&pI7O zf)fW@twOnGa}x~AK9WecG8e3CqVqIcz2G9*B1y^i##r@;!`VcZ(q=ipNe_9*aJhH~ z*-n6Xn*@WL6Owo)7z;!_(qvg)<0aTu=(Yh-mTuQEPz~%nLVWT~b4cLEya^T3Nvp}; zM;c9~CD$;{`p$2TB@{uno$Dt?mT_79>tFek)RI`af&`os5Z$<|*pm4`x3ilMCR}1= zm%MB$}tb29A>=@3XR4SI85CtaapcQbW zb)l^RWMc_9cvHJx?|?m7PY^K~@lSL3wzz?wUB1F&9;V)KTrc*^ifSIts9=-%MBo8O zQ*gw1c|kFF(=7}CL{EHsh94M0Ox(ffi={F15ueGMAvj_DP0@#~`Fp5~mfTNVw`&Lo zXpduURI%mUhfecfR-)iEF$4JomdPp|tlQlT0+N{$SFZ8G{@NO{MM%1T9X);UuL;vR z=kf+&2j=|GfFdl0GVwmn;-;22yp&Ds*Oz_S7(V^XxAihEFOW08h2G1}Deh zKv~uB^5Mm;>iib?ue&|+vhUs^%tBb43}I0)L*DJLh8~!36Ex&J`!paN~CW%=4#8d zw%aMO{S74gat8-wJ0Dr<;=JlBJIawHL;QJv?N8~ znhb*fn&?zpr5GU*MyF9+_F-sN_MaY5z&-jbAFcqDgrZ>%ev^v5t+g>12Mkc4H_U$F2V>L!-lZc$9pve~#|k8_vwGpT*?gWU zh&U)25pLmweiUPQ>U6<7_VVT|8}Bw3Hk+y z7GlZsZ+)Cz#)5CaD#*t0`l>EeF+4<)FvAM?6Y0n|7@vSY7Z>x>{nq?8e^#2hm4uv$ z?))FG;Ns891Qf{jAEPN4*+7I8l5`!B?GKOFPBoy}yJ^m2wCE0`mW!0vRn$S$S&s-` zDxWzCiyVbBMM)e^EGcL7U7u9kSib$D`8;^A1H>@Thx@4G0$wWbOx0QM&d&I%Hrt%Y z0{m002nXklZpgQVTyrdbqn&H|zHew~M{qD4Yp4YUI0jbn!vEgz3ca>Tn#&NYJa1jD~NzaTSLas182lMKV8`4d%rYJYic$Hd@0*d5dISd6#2N`YO~py!QQz^Ln*hE@FFavg_T4CX*kV zrS8Tg@G8olZ)k+i0YJWa{m@5lun4zHa?iqbL;|D&kt>hj$;LhN@3mFf#I9J5f)f^a zaxA0XpO&DRygx`IKUanOu+R7w1S|iixgcJ^Df#F=bcWZ!pNHytLFU_s{R}(>!#nnL z*B^U_*t+rj?N8!;DLVXq<{wV-47Na)juZ?0OQB823N@6z{4h)B?9ocKv?;THcA3Tx zu5iPO!~)*(XYyzPDGMz9AaB|9lDb&!wmF^h=WZC!m!9Rh;C~v;0p&BBJ1|j`fa|-x z82zysh;@?9WG=IZw~ zhZVnnzoh#vcAoEm2=?^98EUMEthyl0AqnE*%K|#`gCCjwM`M6-YPS=zc1$DVbZLi# zUqY2wo0T$0bZc|=E72bXY}a%juvxM(fplhcl&MMKi=acdrUvYJLg)nlwUCP)Q3uf8 zCuQBYAOjmVCZZf$XmGtT1HFUayy)8W%D9q&xacaqax4)i^a*T7wfg4B%i$ghG5m4RHk3ULScjS0V~|0uN^(&l3y0o``c2@WRB|JAyzHxC(qHw8S? z)H*GH@`06vi%;90S%aIK3}!QW#YUt*n1p*)M^yusNxrF9F0wAXq{T@Axoh8wRAap} z{{X?6?vPqig;Ou|_G?a4z9jn7no1E8cZ+{z!s%}>V zGjFLoK1tl{665a>EbfndlKLrlOvPm7t-kszzn7BHs>+;I4%&66(w zmtwF@@@fhE9r2>DzNx5g@)7k=L3OPd)fR0l6U^3K3ihV)44(eqWFZ1X8s|hIrzx#ha#(_rG{AQAg z1*Mke*|90|tP%1m=+XS@X47xE%)w<3rlv$iS^KRZ=9=gkz{&0hp2o?|>mQW(1RV4b z0r$xXBur?A5#T^*+$#94#Emz|(*`r@geu)t>lEPQJBFbE1PX(Gl*{nm$*myZ0j1_} zk#}nYy=G!_ed#hmt#RnxQUBtX6kZ_{Z|!%9?kn*m3LeA`WD%%<+1%i;X>Z*jv6fbS z?H@fABotNgp5co@l&KcCB&0gef*FYL2mj69SNibLDYvYCWD z>dyDmj{ewF`@~D`HdZAF4$GO)Ss=Kys7dmFfrdtPyj>c4!Nm5pyc64C-kA2HOCm7~ zv~L^nF!(E4F#GTb_Rg->EavZ8JPku%kgY}4t!~+fXh`P7%VQ##d5tmznM}xmUfgcd zW?opToMYmy7)|>AXY*lXyDPVpJA8Hd^y~|W1Ik!&#yy?coJy;bOs=a2UH#{`08!B{ zme|`e3bRWkARi`B2oX7e6&@+tui(otiU~$&NZgsyxEKA9>Sl!QbywA(zYuAL4uzGT z746;&Gi&U7!8#DLc3J^58SP>-D5|*jaG1^Q(~8Hx6hCTPuRYfvQGGuaPA~Jh7Xy^m zKkf!g^qKyr%wDU(r5Q3iB8_68#8%5kN{)e=f=;A_eBM0V>) zC_(e?ZZ!q?gP+tpk0v}KDvofBe!W5DVfJ#;Bmo?VE};bYCQ$E3V&Ev2)vE*z?96RP zMgOK_>m|6RQeQWt9u3Ac)Rl`OMqw>iykr>2`gI$cVnG0e$0q|843Dqb5#|wX%za0{3 zEx1+w4FEX7A2r6|CU@9ZuryScSL7p#J16LZ^nKISPy8=gt1TALT&`#cLrMgj(40Xt zJ7}jquMS~kukX`4F&5iJ8Upv>;Hl(e2HZ7Fekm@tg~>sm@yFdyaz-|)Gbni!ep3P) z#)qesZysggzo`j-UJacVI&6{mW5|2731fQFbeOr~$Us(Xbmca~Wk9CIbLifNZGO9! z!wp}9*xGf1a0a_n%XR2McLVe8_*<$XH~|1P2}js%S@l+piM$SJf@D2N?s-Vktq^86dZ4%i7wJ7hkIAyC>vK$`a(zScA+^)2GFS8ngUFuF%bcTI$<`**SA5Gxr zBxTd&WeJh=?EmUmq`-ZNeDY(358;K;HAk2_i?3>MJg!UNPemYo=5~mxJ#pKHl+Yu4 zTRJjee|6?+j-);s7e6eQE|O;vm$YWUiLOW2g|jZ8G0sk#{Fe46aBSqdlG9ojr2 zW>xlZbn(Ep&nn%pp#H7J=a3Ki!^*r#fu&XOg3db`hgfyxXUh!_I%mU^ZKDQtwOx+Y zx`3|>u^92rp#CufYiu(R4f!S2SVs-ePcS1;1VQ|n@|4|LKU;py<-0iDE!RQbydeCH zT)}UFqGL+~VsUVpEA`KNg^(Yo*^;dp{c0%L1EG9+o$=JhJg!bM7}t_x8HqPzDfOJ) zb?z*Zix(?3$G}xap6^wV8++Fp!^~!9{PLU~B#J{)@xDhyDj;d1b+(BXPnqZHIA@@i zr@e9w-r%siZqnu1{}1|s=U`h9pW-98_I=Mva9SDdV+i@6W1puqH zUJPtkqJLYlH>Q5-sCg;>HRfr^%kJfFY@}eyVc%p0|1aoNWFS&h>Huacn_Gfs!40Hb9MO3Z=WizORJ%p-5x2m%;4Kc@*ZrXQG$h_CQAD6d-=c&i9XHXV3S|h`lne zgpoLznK6`KcLxjx4)0SZIfXJAJz6Y^Nu^CiDuZ=!YJZ5!p%_3#0$@0% zAA-P|9IqQ7Fd?JmKl&~LLtYZnXD%$=aZs9J(R-c*(}1KwOpkaj>BLBF{;Pa%$$9ib z+M_e);gdXbf!AIil*7ZKLpRNSeB%*R)69#3XfH_;S}s6D=X!=gFYsM}^#%};smI$y z#Ug2vE=Jm)W11lBr~pA}cM(4%h>Vy8C*^YwH=w(63>^yhVJHRo=_f=6&G;8Q7N0pP|e;!(R;lQMugx zEd-{;(+j#RFxf0Pe2TaT$a5U>rPl)H2uaOyI+=F-v)8Mt=zZHS_OC2b17XW{8NVYt zlGC_G#DN&`Tx9)}r$pJ&T2i$yPb3QKI4Z0X*dSVuH&#-EWXDf{+zM8bbwV!!)9WU}vTuZ$=tK@> zmT@w_PmBxR%25z*NuWnc^MDAwdp5c6k#(RuKvkMhkj$3 zW_r==EE*H_GjP*Nb4hD6gT>P@Z3dR(+t2=ZJtX9!OBi%hYtG{C4y4}BAMcNucvxjy zs4dEm9}L_AI*QC?Eo@z6S>6v8t2(AV#gOSNAPus{@~g=IRhnK6JkDz2RPBuwgK6g; zLCD2jJO^nj`ca(3y%q-^?9kWlVl1=IMg=C!GIeRw#^fk59GuHM1sN~=3LZ%@eBV&n zOBuhOgy})LRaUmZxmy!=Njc;!u??&0qQIA=hDnRImgql1weuajw&dVJwCefCl_W~8 zCHx2A@MKkWLNN!Y0NXQ4{(W8{!h-aWDP0aC!~;jPIgP{lYAfgYFNNId-Wnt`IQMKcmNqSAgUr8 zmq8;(1fFnu$9ybTkvdx2>qSvkF_ZWo=@~{5D&Q9-z^4~(`Jr$tgOP-%Q@?X3AB!-Q z5mE929_NB8Y`pGomXRwJj{pRxx(%ed-Vt2B=m;3*%4IdS@%d#6kQsG>EVb?k3=C#{ zvn@pVA^s=c6<}S}Raw~EIT~YB;f~^CezF#W!q*j^x!>`J>gLT+5_cd~-%6jDmlGEigULQ5o~iY^ar`#MxGTh_IEP34nt zSuXh~dR=DieTxjvI8}bAIy-s=DiNv`kmjJF9K&hg%GQb<3l>&R@5(2Y?rdKarEwd! zJAB#kB>j)wf7|osxltNAJ9Th-m7|_Qv^|}pM_TKrd2@5i5^}n7$WdY^#UROs0yWU1 zq&~sWDD%h9VgZogXhV3#Y3Ky1O~5jRvkxsof(V}v?mwUZD7b`%O>mtcWk{_ER_c=8 zFlE%yk(Nd4kaKqE`_)47y0ox3S(0&%x1ZLf|@m z5}!=kSdywFeCSD?X`v4>3n#qINcqLG?;YnHd63G4ztd*qLnvgA1D*+Zm`#xtI8A2@)q95QC5{N3OhZ{?ZpWl{!r`!8iDDR=>QfiwQ4Js?Cfx$u`XDg%0x;4bY8?^%BBRAx?U;o_1o(Rrr-SyZ zZGV?Ax5ygL<;jj~Kx(!2F?;?=j3G@0drvVwCg>lD~qXGZWNCe zOg?fOH6=@M1I;-aioDXZxsv*V_!1GJ2w>d~YaaH_bGBfFZVEznr&~{ghW7zJp${!z zmX%Mum#&ia_SfU^W1XII>o)z;!5W^vatFKsDL~f0&8GF(^(ej{& zv&Zo9_yz_)5NbA>>mMvoXN@IavH~tIaj%{M=WuHngY=XH(FIsZEI)`N+QNct(|(>A zrsmP=#p~9@-nb92+SP}&&U>{eYxNmE82?ST9s&%0@)4tWf9VO01odN8tO-#vKCxRb zDj8a5-61gFJ@U@DwJj&#w1`8R;WcoN@d*IJEbY8y))#Q7ehAD=Ps>*opv4Jp7unQc z-8@B;ups$A$U#W#kQTz}RagAj707Gx$8~tcN?-Cks&Xg-l+hU4sa0~*cRmRVb2C5At+m1SX47|GA>;ku-9*YI?jW`h<8R{P#$l;?^>tz_tx`wcAD zKxm+7os|S4K0W2cJ~lrBF)_%C6FSLYA%cEI?PU%{fT(~lZh^-(;9HOKwg|^TnVj-c zLP2$~ln5=t!lbV7QZiXZo|K$~&-Z1cnC=)q0VTu`uVKm$eRe5cI1IgK4f#+O@BB!v zz@te`94)rYxqu#ho)ySps>y8cp+f$kX1~wm;DmbTa68;0N;^Ib=M+|>h_W9cJ*CBx zrD6;eIH4M}`|})x#^STp3naTk9)hvQy%&;z4f%C)D8rby8)b3IXch@`{^6LL#gI|! zI_9yAN^W)z(j`Tgrpu3$OM|f>?t{(f{~iM9ypr#3d)pXgwD zjwRD2qhYUEi9I`vF0??xN3lb~L&^L&>r7Mc^MEg!+s5i`T5gxBz+hAf(n`qUN&uQ! zCxlX`$Wk_~BTE+FrP6nM&BO@?1ta+3H~_|mtB2!!`&S4JlXjP*RR06rJz?qUJ6o51 z|K~z2dG4`>m5uc}lfUZfFaB8sef}mtJ1&!v#Tv9v*znV=9`{MdFNlW&nwI4NrpY)| z2((BNik?TPI0My`;dE$)#6Ztb(FY^{qxqAV{k!e;-;tBHLHxBy0yk8(sj-LDvLp%H z=ltAR(-Bad*fDZ_!l3Aq8r$!xE6H{9n!L_rP5RdnOX?f~|2kB;EkHRutv%dv z2En&67Pj1p*6gJQzeyEJ6_w@;Rl?xd0+1kN$Zv0^D11+cf+xvA8-G?-7c$pWG*nVaz21|!0VNFfJECLo7}&L3~5Gc6Os+vhK;dFyQ-IIbp+WD zP|l*G^;?)j5^e$PzcS@1wL1X!ga4^%l&C2o;*r|aSmB82jnHzwZO@*0ZR((%$b6RtXmdDUe)QcO*!90q+n$fNi!JsGho!HDVoW1u-xVITAr zg+{6ShU?sxQXx%bom)#tINMA^=fl;P{V#XB;Zvr6DGGpJs=2xBbk(E`~Nlzp;(5 zYO#K@*+sG{t2()#JfoNMPb8<|7xO)qRh;fm3CRx6Z`BbXoi} zk*3e)G;3h8I+BuYF%)JI$S5BB%@eXpWFSlvQ>i3xCX~DN+>juG2&Kcc0N>IX(ou z#(Z(D_f?8qGgv$b0Ax>8JVlQwVY+e3v))bfnM z^Q_5IOkJ(*WXD;n!#USspdsfqCF1wcZk}xb*2Ak#(v#2|6QGoubxOaljPp?1{nm() z55h4thYt!SHL@fgjt6FRhdqD9j^A2Y9)-$F?Rz6E`AA~R@+bWhF+n^+0)rs0?gjlG zmg;j3nP83x{aq9=8&IL|8mPP+jWAT%=s;rqey~6>`ZoMp>HS66E5s`v>o_!ap|nib zYnr=PEnGsH`HM|O*Sn7Uud8%O&$G#5l8JdI?{2&{(kK#-HCZy`h^h}vYQN_0a4QWX zZH?aFb%XS-hbm0@4%9^&D$)vHx}J3oNv2-snF5ZzAm@tS^;*3HKsdS~%#|NjDLb0P1{< z9r+~==SUc>t#r*HVh_iQYRp0QMJw?<@gI2=RQ}75;()Bq z+`Ffu${v;X#tH7aRc7}__6i?G>n{{Tn*u>41fefYm(H_+&y!M(O9MnHkUR9%VRpdD z@cex4%0+RPd1;-=uRr!<(1Nh#-ocA?Th@D^KPCG@B&g)o*Me!1VPRk2}{b;A7%;9OBL)h6IAKbp&Q8}1#GcQ_TVEl(=6 zHGBv%{Q08I#wMqM`Up~H)-RXnY910~g3-U3H5-q^us{{pm6n>D=ktLbGM>otdqF~C z8bQwVjSOhVBn|X|Jp_OOQX&Fzx)NqS;6th7Yx(spPgxCcjIkF%U3CEUjwN^lM z#YL9ou@TjZ`)q7rEPoHtBIjwc=ssMtA~s04y_pg#L`-OJ|PrxrhUmyDg>O6CReHOi(UQRq9uQHUTv3pH0q|t)u;O0?dF`ki}!lu3vd?9^#8( z9b#V3?-aW21}V}cBH|Th#Jf`^eVG}80Ph>P1yGYH>fr#)-$-(usM;4&Zy4I@=iXU^ z{KS{h{Cc7u*W0ilsNIN-j%f-6;e*i?!GsR>7m&y{Rdna~Hu`cUIE!o#D zKg3(PLj~Cf7+X`!i+uFJ42N4>p~`Rke#O}_twZUe_l&O)d)RtP z6{V2oVOBE`xp<#q;_bX8Z>AJr=aN)ZihPrj@fdc-(EX`_nw~w>NWqeI5@sAV=4_i9 zTk3eb1cFDBW#hFRUwMrJ*SE#rL+Ac!Qs?5Hz16)TF+lp}atwa+Ix7qGyQ<-i!#Cmz zfonCUC*rlW!)+Aav~HG_syg?&eVY)#C6fT%ju-x= zf4W)(|J_Szm*-Af3HHiA{x08vP{==o*fNTmyt;-*f%M;CC4ZOQi_-A&6F}%iOL(l| z17I-|3fgpRRT_@gE4k#NjfpM~3M|L~;0AB$XuL=_xU6?SUy32SxWNsI<;5vz2m3Os z(lTgT)8)rY7rM9o{}_?@7kHh#Da=W%)o$cM>ia)P9GqD4E&+sJz4)Zt8(S&cb5yJl zi!^w7&pCPH>+SBftz@jOo4v-~=#_ItWtu)WNimK`#of@Kq?9hf)F8SqPq7rnAh>&%!z> za|h~CX?tPdsAE!BYBYP_;T)FbUfw0%2&6smBr&)NTR8#`{ZI{8f->pg@@<}5eM>Cs z7i33JMhIVPpVj>!($$(N!Kck`;>VRoc+e--VTkRsY7UB|kXey*CTogt^GJ!by4|iZ zuh>h@=^ECcS{%}p^J^3mA6AC#tOL;}Ke|I$3h1QXT3abgkgtZtv&vUhIb5t43Ouih z`O4csnXkP=s9N`#1z)bN5|qa{a$prd={%{PA_a)*D^cq+3Z=n`-v_rl zJP~!5pjb5RG4my~v8EG^W04l(;`&@cmiv=XP_xMc`%-C_y^ z7(WE80POef!t7=<^haI=q&*-#N3T5g4-n*hM-W0^wRO=OcB5|SMHN#u#s|<60Mjv^ z()Pdny+bxTp7f7DWu8)Fd8F^#@=n5Naqc=_YM$h)$AyD`DnP0F3(^4KearR{ z4k4YsC1mTqE(3|ZeHarX!|&IF{BcbYf&0}XN|cqIrUnDZrKZ*sZWu+FS5;m9H{H{s zO-7r15TxY=1E~dum_b#F#Y0L#Eg57l>Cc97Lq2GnEJg!>U^b#O@%^7=#hL*0Is|`6 z>;$Il%I38kre^4$f+T_@u}*{Iwz=TI0cuIMwp-c3id#kSE?Z!dxiQbMCIAiqWk;HT z-`ltcQcd;IeVkfwS&}?*z3!6QX40f~8(S&?<)S8Rw&|BEtg1}V0i$qP`ymOXwAmBIxOhkH z>)Lb)bUxo8r35a0j6^PhhXJdIUvfj7yoEnV((7_)>jhnrz%98`NZ5_ca29zIh0@|; zIo6`$2XmHOehZAdMwimRK`AH)r8sw%MWIt0?_*NaAoBOD^3xFy_c(sRvCiTiIV5 zSx23dJ575yHVJwY*swYEo$p));zMGU; zVjm0$lSQxK>r_vF*^ioO-UrR7F*iMZ5pW26NH8^oI0k=|0BaI4SvG+zoj*K#|7hhG zaDrZA3^2$L8EL6Y##$X9K?vth=FGvfafWTpZoZQ)W4sB;0mJdO6{aAZQ1K0U3UWKf zazS>L=dnIZmpMCJnhoY(VSCwqS1YLS1u(Xd`b3Y)D!FEGi@8kI=G>M3(eJ_E5+Ml{ zVyKrQZX2*?|DP;6IcJ(N4ca;4B(UubVh{;R=651Vw6Q)H$2GV`bdL#U{Nc#QN?eYp z$8hog9P(yfFY|R6{s^w=$fEKV%BLo+*>#^}iUCvbX+8|;I0^;!Re{mnI^Wr`UdP5I z*+%}e@h2CV~0f!JQhR>w<*DdniKM_^nv%A%8P$eF|#56+}zt;t3SFt1cf@GPqk(SDon1hnbp6+$Se^hsg4A`eHg`|$rm8oTg7geQSa)FF0s(-J z0mahVIKZX$Aa&M}^?108Y+uXwk|6c-Jkef>LO0p5Nv?K%-Xu?dSSm?VKv`5gJ^S&M z`!|Gv`6IHXbnrZlDj<{`MfvdIRa`J0syx;D+Jy1z^Y{0&*2N-;UAKjt0M@~?u_^3} zbnZ}(!Yx0@6&I&7i)Y1sp1kGt%6gpW6)6Mjd4JA0xTNL@G^Apn+a8Ur_9h~1{RqzO zn~r8OLl5daxK2r;b3@- zC`~2>SqaR`#B72~GZ(n8h~YILB5+BndHxd?zhpIrBu{vDPgrp*T@w;=+f z#+7cbmMX0#y&g$+D{y=tOz*7R!u zVB1m1B{Yk;OR2b#RQAN0o>U62{oFD;n3|TPi7(vT?f-l0)j+7Kz*yYFai-=@s+(yU zVwREH=Om;2*_Bx(OXmOoMWpitP1=B&;%_Ph4%r|Ea}5_|1HY@p)Vl2s9^SWsLX_5J zcZh2s4vWym-&*8%f9vWH+n7wLgwPKVKeAjtFFF&fSX%>zZ9nHnx-De+WXo_Qy@PSg z`Q5hUv1mKss)re?GhWfjk%uEV{8)$fz-BG zHbCb6&I5~NDIZz%a;~zrKJtT6k8jN|N3oH%Zn1NrsnNV&Q<{uwFY@DjGON=PYb7>8 z70@vNCpf4X9AXF@z`L}sjvN2~lKn5K)Ht97CkP*(N^QViE6i8YK$(qg-t9~LbV8yV z<{TH=~* z?apI+c&4T#wMK03g60cuH*Nc5O|KSt*$2@oaHlE+vKrJKy01e~SjH1iv4wZ}Nz5$^ zSsYeTRk$NVY_WUlj!s?O)KdfMe|z1BUB8e%-J9aNafg=+pR^rkO=BzVMD6+x!onUd z^$^(`M3(bEvAr8)c`^GS)U19LK7X zVsoLOQ<`2#3*8GDO9=l$CH;;mx{wd7&RZ-rBlQ!Y_}Y(zZ);RKK`B`FTl@kid!?!V zjSXpuTd|_0w0thr`1P8%Q-I&hg>rq42_NVo1OFCF8||4DVRPs)GT_y?it&@HuaD6( z+>OvBr_df0We5I)n7z-V7RVwEX^WR@&xfs>{pn~6^?3M8Ln9c|0Fvg|&zgIKG;*XR zQq9AH7ti$CE&Ouj-^D~{fnaL%^iJD9KQnyW4DKkHePcAsqFPg?i7OE;>mCkCPaR5A zBRer*XB5@ih?Mot)$rmCPxzNO|=iLhJWZ@Foj zX(nr1EHkyOcQ<>mKvls}jzRb|a(*(p2_eOF1(T3$KhYQz@>f0Z%`o8H!yfPd)VL8x;tR z_)?u19!D}=-~JN$)la2_h{MS^qWAb0{uev{gH*BQp6{LD@-!oIRQhYd4hUCdIA@Ax z|KlLDf(rzr)5dw~VKJJj;FG3#v+y14$&%f3I0t>VV{AG@BoeRR$4dU_v?BD4bJKz{ zyvH-K)`=ajctXy(tH215y4h*|O2?@?8R+VyG0W9`Ln z2asi7?N_h0YGbQdfoFY)Y6vD;LvjHL4{UCZ@$xbPIWdQJh<4}?=`rW3d&kD*Gbd4n zUl{p5?aJOsw}g)`Q%Sx}4jihp%+^5~%vWQOX*mYhuqlvQp9(4#$w$S1m~cf%*E)5_Ee$I^hF zN}~kE(i`1FfM82{n0O<-j=!pQclW@!99TkhaVa~#5a^hvKAo_PdqHa~a11X%_~6x@ z>P4NK;+DU~&>1`<6FYki>d~O`g6Q-;->~#Pq<*{e8!BFQL=hO4BFDdGDR+tpy!)Qx z^S+h|loJwUJS6o>JzoXFquY*P?qI_mS4vHf(zhqM=ID#Y$}0CztY;?|G*a}#SJ)y zUZufr5z_1DD1r4Y>2cG2n(UF~?vpe-Nu~_J_y}G=X#$nBW#)6WU{3T5p8>qaR9WUS z&tC^-bu%BY#~zdWA%6+KV;VO+ zz|H_Dg8{LFNuJ>_c_uB$elGki)3zDK&w99?U~$1s=Nqtk?s`eBb78>*^M9=dHC!_Q zStvg?+UT1aK3(E)h5{ICOP#w=I&{+xscR`v0BWhMMUr*Bx4afy_U61ip1P$YIp|rY zsH=l-P11k^?2@nOo5gL^_q<_a0t|5)s>AujI?UY8 zH`Syuwy^HBEm85cjJm=AM53v%38nN4bOY)<_mWX=Uj;WM>L%eEjyO$zJD7JDnsfrf z%Rj>CR4t;++0`Z>1ubk&Z=Z_f=1B8SSwNTNFF+9ySsRMkRA*E+c6waODyJf2T{8vT zXo=?>yd7D2rJ#CyGV(L@2R+YkqKV!vc#xD#J^ifN&)R_))_7GN!X&k##witQ`@{;v zTpS*-*Fz4=X#%n@kYjtJUAIyf_moAOuNeMQxrGu<+D5F~yV%2$IhN3!x4Nhn-ZCUL@zfg-hFWrnAXC6F-5GMa=$~Bi1cwchOggIY(NX;uW9Y z;(btdin(U~@ZdK@RiTxmjHmdnpD6lW3IK++`*x1N6E=ggOgYE^9|_WoJ_b@TOxR#< zmg@A}3&ycGNXjuIeR4;&Mz7642x&|^&3|hA_T8bmxKT@Se5W)7C8lzcMuT2O&aGdW z$4|!MXbIJ5&;$;NNk{NBYUeESr)p*Mb{`NDHQH)qTW?z`-SmKlirdN>+MT$vA4y-1 z4{4G)L~rH1D@U1h9k{@ULnVCGifaOaKa0V8&qn#_bEhZ@K1U#ellFomD^wrN2O>P< zcC*MB&N91EetL9#3D?+vUb!228ZOXgnHP+D;Bm+*U_|oi!8WC1+UFRlwM(4<7f*kx zRefqYtB@#Dvtbj!P6?tnm_f{-e_%jiBOijnGvWe6psO*r%cS`Wy1IHkMc0y*7d$~* zvMp+kc{In+^Pd$QP}At4kv8bmlyHU9b&$6K!n(9tgzLXt6j zzfr@y#$g4OGSaN-mZMmC9_ttI)qh88{O1#8DuFTmz4fSlIKv@JO7-G1#Sbbz{!B&( zJAxNZ8jIb=Bf!{p9dZZhd)2rhdg)!n*`VY+tXD7dKPVXYeNbyr-?o1IcjK?C{ z%;(JVl9pt&w|zI}@5{sxP=u9C133~iWgy4?9nw~e$im_zB5Q~nw{vj2&0N&4>(}Md zG$eVwG_DeAg?~(C-8Fmq*^={g=b}}nf+L_3c!MnIFiJ3$!>}kRf&$w?+>}eN^;whX z5IFJLUM{qfubTt*efj1QEc;DAmXNOG*uX5MSesKfKlLgLn{7*QsBwy$wadzBjaLh< zuQ=DYGbMo0rvySFl7{jIn7h~tg*Ch5$CDfV?}`z%2y-0*kMDxMNIan^U@7p@IXU#8 z7ZUWC#&D8eJ3C1q^Ch>g71&Y#RR~I4yGiEW^X@C_LSSmSz!>u{>}Z5*D}t}V#^`S0 zN9m+Oa(p-9-scBDGlhs2w7Q!Qzntxb1hrMYRD_qX+5|+@QvMzg%#NfoEtW7f$7s{W zb&@{k<7Dlo7{ux72MJbSg8u|mQ*!8^KlR3D5LsC>hD96x!A-?zh70^*IB{}?j03FM z;ocO*jv%{~E3z78(}d|%W8W;i4P%gE7mchNCGdpqC3J=OFe%C>iTF;Zg||Cy8xBIY ziRNLKIt)N(iy}CQF2RCZ51>qE6ALmeo19jPSh$TAraZRRb%3oP_>v8r_Q`hw^$Q1G zKq5`cJwHBAb6XnYa=R$78%>(~45??1RvH?Q=vjsJ%hUbg4 zXn2f!D#NUU;KiJ}bokSLUtY6!3LPo!{vEW-WVrOUXB^8N&>CEg0-r%Mi0Z1pmQ66mY3w(<&-eyv%!)Z zT?8tb@D0zL=#f2x$)<3?xOZXa$87s1@^9>ee9L767F)el>>vmauQP9DhdEW-P94rv zQv`HD=4tA9g*8QF6=?`EA6}s?)9T<4!&AGu^)qyC`~rVOSWq@DX~w4WefxaBWXJ-( zOUgW+c4~9O*})8DoARw*YO>u^XJI-e_27Het(t9q@woBSs&(cEh$_HE7z_nB9x!tJ z`oz*(n4rI7;oJ4ESK$sVp!%CW_-3$YU4P$bigJL{q8xs{+OBXYR4#`vxp;FnK|MIn zZRl6O05Z&sbm>DiUMU1eDEKv3w%N9~rx+}rAii4pQQA*6F`AaaZbw-Nunu=*dr5n} z{%KTjcpgUiSG_OSbBZnYX>lR^VTit=qzE`+yT-H$d7REugyr&a#TpFf7kXtrui6>7_JevVnP~LNf>W%^KihA!ymsi;pUc+0 zn2|s8d;q7VQcbBEKJo^s+3(EthW5lMb?`FqEzY(@EC^?pn$DANL0C>W4d4|7OEjl| zgCd$}Wh*iieQKgs-MJ*%W^R)p5#<#`If+OEDtUwu5a&2X-?ql#>zDinh-{wq`OZ7d zM#r?&qZpT@i5lHe#9r1;O1$PFJRvl%Dp!*4!Y( zu{1`ZE5^N}$h_t5GVaLRxEXZBm9Nfgwh6}4BDTw=NwLtSKPtd~Xif}b=-(tqN&5&*$ejU!^4 ztYEa0?NQo>nU-TvLXBnoqp+SMOh#>o^KPDYb}ehx>=M<(`8KuX5|uqK_NJ_TZ5w;4z-miTvZHGIuc&j=e+icWJ6eV6)Go4 z%rVc=W+XvEQj<5!k7{e$}$VX~#fiD$r$LcnOM$KPjNbWUPROVvR zsc-!{9S4TQBf!t6Cg@ub9GBGrjo;nojYpbt{rvve8CUoI{4*kn#}8#==l8cBE&rJV z@@bd&2O0kRI5+vl{hSi7l{4Sq!y^BuQ9BS4t^_HP&U+?GmykCD__zfsD=rLnVb>Vb z%DwyU8Lqntzh^$Vz=qNzDLP$nqZU`XW}v)*Vr(3SnzjY>c$-*`=v;MuA4$55VJ;L- zg_+BbBl*!Iz9r-E!qy6;Aw#p(?7dxnop+nxiJ+|<7u1bH_Pj+mApS`ltsKv#4S(;H z+|%h?pRZRf+2S1t#y3X)tGxrYjUiVY`a<$MaXHtHtI?tQrc%c&i8&)jD4}-g=FRo- z;a>YoSF7d&$qB69@A|?$nUp}Y2BD(>7}$=fy17@wIz2m^GV5Dt%k69<9V}eOWmwn1hVaffxj_?Yd>P4F@VP|1^eBqs;=Ei@7F# zyD7N2?%?c8Cu4`X#N4NQ@5A+3h<35_>|WgnDW40W*q16B0l=4b@${D-h5;}++lGWN z;GRZ7C;lSPpLunmh*;}1vM6%Ucq(R3SshnylF>kNB@RqAemnpKP7D7ZLW zs2HNSg4U6E?GedxdjTi^H(b^!l;nlOk^0Nd-N+ftYT!569JAChNB}CWw{v_2(*U`B zbluJ^NOmJc-n7&+YAWtjUq_dS+&neD%jW{w?a~#c!@z%O)jJLQ|cz+p%mag|!x)y)i=YtG#lG{?9K;^^6*2Q%nSKcJi<{i zkvsP^BQ$;mRUvByhF_p3h2Gd4TuS~R%85eTv^r>cHpwYmGHVBLC)BQO+)IC|v(U*k zx6U_}bLmovlPo#VYMZ*tKb*+i5wz3@pJGwzSW>-Ox5r8`Q@C_ZVXO!yB(C}~4Z21% zPmRdO8Tw9={CP14PFU+&u7>mMm^wai<+mDsA*iKLmYhq~#hY&`hiwcP_*l;jlo}K} z>;qzY{qQxtt&ssZR)fjSF!`aT9g6>PPhqW6|Kc{JKMIyI;;7)3g$pV>T$ICKi%O4A zyay*)32gNlO2#~?knW_|CK+Q;7zy^jgF*L?j6g}ao*8SrIy}Y5L!L>4Xt^Cif=Ap@kBYekIRK)$vx)paREl({+k%iG6p1e4 zAr!+-)%6isC}Egg1M?i^@N`)hepuNU+<;TlartB+Ry;!Y3k za8q7rAlls<8#7$wZj{`wYwCH3b?bGs(&YQPZgG;i6y{&MrRo)cy*ghw#XQLAy$Hsykd4{75x* zBdR_)>vbgPXRK9Zp14}5SPaBVz6Q#*lnOP{6$#Jw+d|AB19^8BjBBHRyE2pI?C6z4 zl5qbPjJ*ZtVa8{^r|s{{gA9`Je~yRm?AD=^*U>lp3(Q z%G>-zZ@lhfnTOXc-l?nnowFvjdL#xF(JXOp(@4V?doeik{lgE{o3#E)*$+KG;zDe1 zc4d3ejm{$3X!+4#xYdFWR8%T!XTCRYz34dEm(;7`K>z@3W&U{x586nTs#H+E=c-X*oqYDSsjX>)a&% zFX0=Qi9F*kYz?9RfItZ;nsaY_ZiNbPL*k6Xli>bR9$TaIWXbhr>r_b`v?iqu%hZdF%7`TURzK>nJ(;t$-YC$FoaR;k zsa8O#vlXQw^ZJ}%$dnp3defD5CTh&JKk`u*fCi}HF{lnN)f234A)x`?1oDgfY-PHRlj;*{pAsO%frF$+$#(^xM- z4Ag~N9hO#>N2Yo)5 z8kz+DFUzmY;t>RhvO8Bu0+^V*PAvG??HW@#g*&yeqnxn@qdb(r>wza~Nc1MNitjWp zryFdZ=y!)g_eM^Fu*A6X9`nuNk_0c3XTAdGqpskc_DHnt_H!>spo|$| zTs%B^yH(N)ZL&Xfs@4kH{|Thr*d`s7SG_Ouxnm*7uK#74us|IZE@O#=ad(?oJ5C5u z0%Y7;op-0O8bew_nP zc`byh!!fAnmavj21*>wV2z4=Mj4wa%fN9-H0gQs;Ef0IQbhhjwpS)?eigA#e0*!sf zBDG-kTe5F18itfS9TFsPF%#4w#c7RGHt2Q?81v`T{^rNLH5-BO8z@a3>-`mO5oR0a zmcAgMJEWEJ@N5yQlYP%;i6z3pW@fRLpwO4nYiINE9`-=jBw!g7GFWn9-K68LJoCY8 zTRKF*(hRql4`18omZra{to3*Nw77&!GCqlzRFfJ&S`kMn=b1ef*PY}W8>RfGlv+x8}>RCpUVcK*X({~7#n6urjqfv{|(7?DPp7%JsAc`wC`h3pTh;Y9Ud zr^j%#F~{83i)*o(~-+pv3sO<{g~?ECa`BkpcufiP*d0|dM6ww_g01oxg) z;{qQMz$-P3K{AIJV+M;FaX!Fx*>0!0IQ(jJeWZTjWE(e8>p_6Hamxj?@d8n1nbNS_ zAgnnMOb-R|a7tH@6SNitlJ&VY$9~o(`Q=RJIVS8nnUd5XR)Q@SUvW=BR|2`-k1%DM zPBtqCWf+v`B9VtKen;ohdJNUF>wnH-H%p4>GP;*Bw`Q)ej~lKXQ}Z>H-3XAI^x;WM zdM-!{6+9TmaiIkzKAUJp;c4*mv{7!-aYk{C?yD6PC>o=-m3_l8lbbNJa*iNu znu(csc^=)iPJu{(sqw4LQ_u3ebG&8G+j-<9!u$93*UQ`^rmR}L9-|(Pu5tDbN^M%G zXJ9m}-i_Vsufy!OtzW~$eUaWH2HC0@dO$Mh0qG|DANsVvTF~7O= zzy({{YSRXRN3In$N%aZ(Q5>`7b20o<5Jl8L7~YUDRfMA(x8hcp4p-L*)Z(8pTsZKS zR4YGX*{cnv4r;F%0D2_whoFrKilwI{F)O;YYw~0}ThPOkLB0ea9=ykiz);@MBThxA zMf8&&PUgdfx|$Ti!;UD_Y&YRBuZevHwX(eWi#i~9$o+%XE|{~2j~mw*cR>%hJ zV)rwqF4nht_R2U1BTK5|joAecByCT=`~&zVoiSu5aIMHr-ygf|*-;6`w?}2T$o%8; zR?+Q}H&}y|ADz%};l%D;K3AwkI!~@!&VV*|O@;;l5MfIgH(z4K=-#z`$=< zG4NV(qy|AmUuOaVR^E;tNFmPsN4ZKW038~lIFL0ZgrhBcV&XdcyS$1tqweR|2*GEP zKY1TAs-R&@u-(vaaLsK zFMV1dr`kbwT^nU{X&CHtmu4?%*ub`t)TKE+PSFYZ=aH!aF%s?@2)mRhk~keCw0gEg z96%wX4_;mG-!m%+TJq+dipU?QX<;%7u#~~9`dvLzsyIaBuzo0V33egv@IXdO^Bi-c zX50+3%t3=q4hs)L;z@PXglbleIIyglNRzp^chKPi+{dwe`iK1t=P?MIpGMldRrzUx zEY+Yoj@%GfuAx!a9-+T_)YZW>Eqi`o5%f618x9sc0( zH!7a3Zc(y1V`6_#BD7hk2`pGJaCN0k{o9vAQ(C1f>0Auh=J(yn3hQZz*xjR7HoJ^(n|NChIT zS)_D&iO6Kvtg}SB>S`R3*}42P>&7FFx1`$FbP}~erp~nYve=o3LqyPS`X?tM9Kw9M z4`JA@Jqu_O-$1SUVyqA{dP{F(<08n)V@k?))H| z77UiQ!|d~-?jnQ#^#6&k^kpe(;taU$ISl^HZ5^3SJ;cyS@fa}ITm9r;f+qxDLqm`E?6{46Rj%gY!@pBY8JGz>Y0Mc!g z!DhB=mgEQ=T)Npp5W=)x`f8gIn{mrI-6dDX7e4qBW%7W0tVK5r@3Avtb>Kfj3-3!s z@WVqJTMH*-M{TeksE7L357PhT_yCg&MRV}t&zfsasPS7v9Wec?PcEV?n!B|o9vxk? z>~hBR0=mLN=t(;B5^gzrh|^3&*PK0v+fSsIagE?>N);}Ud6|LUXtw{}x4$QS1&>Vpc!CW);#TmH$` zRx{^aKPFtwXECfwJt=O9EHpyTJ-^gotHD%~Yejv)kA$PydVeS4v9*CpH(Q8O^ ztyoim1=F7jt+y=kx!6@d!@ga&spoTk5e*ks%(0f6ET>Q`udqVy=jj#~tFGX=x0d&K zY>-z$e517EFCJW3XGmUk(MC1`ihQs=NU2C0VOlW+yr9=rt97z&&0$vT!NAFvXxQ!x z*}Lj~PVMN$?7tleL|%5|^AD|HfAU%yVo^k%{pI=d15#R|<2jY(MPvn5cgstlP8t80 zWdo;bn7RAas~+b zlE$M~g2M%ws_5RxTx}9ZkXS|zLUFP#?Z4xFf^MK{Gr2!r{KQ-SMBQg#qLs-hOuUHx zq_Mg{zAex=l5^qsm&Y^po#Xx$g_Qd*C1w237cI7tpSykTo}uBbD!G!~R&f|W{wp|I zIyhnNGIsPBU)%2>Te1whO#q(Q{0qWR_ywNfkld8Akl^VP>GgQ!=(`aA6AX*} zG?Ms2us{OEJH^HGXPyu_Bx=WbajkaS5%@-NyoP3wOH5qWj+Ix`=6w!7i&dHPUb5kx zcg1_nB;eG6y&~HEOchcZlC8{YU!y+NJnYTKcDGrFn;`y^-1bpL3=C0 zdR3~U8Te0H-HPexn{$^>wGlWue!XDVI(ir+gg(HA}=uW zFJE(C#7re~2S?Fywkt;{V{hqatKPVmbIw!Qoab^UxQC?PPlu(%lv_rb%9&2x`7=g! zL2HDJr3LLNreH~?E85D_YI`MSG}}|C$C6w(1t&{@A9(`mTI%L(J0Ac`-WghNj%bnf zh%Kol7bI^y6{;aQ-7flE_?3i-Zs`LA{Q*G>zV!3pK(XRL3^3vdRh@}*fsUgf0j7F- zB3S7`5WgI~68+}h#&V7N@0=n5@eniaEJekXu1`bqf4T}SQ4m$(t8{!$|0o!^iUj>jeh1{pEbVi zGhS9|qdskBI>_DVSiT-tad*W9vw{_PUEhRtbtDwTch%|r#qZmSwk#1t+-X^JZ`PD@ zu|1QalZ=$+M{pGbXPr-x_DsIoC&UKN#z-_QUzj9~)=7tPeR#!*ZOH>F)vDXE#5{&-}>jZyLBRZOO1Rs+pt%QVPGIi+jyS6KKGhVq80Ty%aN}edc(@`+s*t zSGO9l(J!cL!6T$oknC2pT&-4jmxW}e?(<4rF22a3w$%iHoRc2K8vc004gIDlWNm)m zJ`M%f9${8uBaw}>Jg1kiS=lMCt%70^j5^=n>ym|etzgP1mgQT53%vz?gb8+5-a zOklkCGKEWVC_sF7>7N3Qzeo`LojJyL87gdS3gV5M0! zD{{SBsMgG`MZn4wE0$dea12#Z2R79eL-m*It&G}}kzqf(f-V-%1-MOU=E65|$C0hW zq$$IB&pbY9q*_EFm594Z0+DZBqB@?bn{ zut9@LdaKImM9R&v%@W~!Y+slSzPZ9;8rA5E6jD zyd6+!Iy*` zQ6V%%XGGH~mdSaP-(J&QW`CEandP1S?3lEf^D(|qFv0<)tMmkrdh7gre7wLQ_D~-= zrmmg>5EieK!)p_d^c4cU(Iq9j{~_!DNpB3Oa&OaT`7y-1|J5M!VM5an%mbQC#~;er zBw4y}WFo;34^E71$!oU;DqXmXX<|vueW)`c-@*kA>yTcK+>uJX*FYI|##YXeHXnxwiGIlG8jq5NBPiwZ!R#ivy?k$a85WH779yY6PbNn z2ia++;MIan??~{g<0c?KJ2#mKqM7BClAQf+&9wVeg`-`4`npaM%7-o31C5c&L?8;HZ9vMmnKbtFR-mOdc4;T5U892k?@FJlJpQ_KV1y&Zjy zd$XZl200iMlf63M3VcTjSp7%eSzjgD7Z*7;NL{hu{Au-wGvcEnBa9<@&qu(v{4EoJ z9RS#Qe@P$5gmYp+dP>y{Y|Y9Ba7u2MVc)(84IL`D*R0pyLXp6qRw;dA`5*+0z5Pt8 zF%%YZaG;;HO8?^)fTi2hb%T`aP)>NZ=CuDdOM_F&+bAgi#!aNEeyP`ZPY~m-yLHQ$ zCJ%>mW=u3ja2UyZkQHc7&mSLE-x9&;1DO_Pch#XUWYVzGwByvtKnT!XvrIiT@V!1mRdO>dAByL{uRaC!VAkMSWy2#$7(h_I!z zVhwsGf1vkbtKc37I=E$o$W4O(vcpKQodiLgF#ww*w@W~);BS=s>(`suDt{~?}}R#rRP@}>!swO?sqsd?p1#v}*4{w4okU2#+VSADqR zvH9h7tTbhr1Wf^e!Nyn*p)~y;Pqa#*@`la`CV9!oQur8AoRYcE770GYDR|Q0bzYX5 zpS)~h$;b#?;!@Qs=eiQBU)9B~GnbbZT2-RvHxfCRRW-Q+06zh@HFy3Kj@gMIl2C;} zp7~*vYbYa)Bxhvn+1yl@piNzWVMV4R;aE1qAzsNgoPTPmN9=7Pa;3yl{U8O=1rKHb z;_eFhbyLu{;!4Jea>qK%w#(7~^_Q5E17Rmh7ld=^|M7vaP2LL;iZ$33H zWXtnt!pYi6$hSJ`rQ+c&(SIZN<{Sc%K5Kl3^piXWVkA=4iv> zI-z9*&sRRst zQKBz;<5=Y3$}E6{tYi>-iXtX4OO9R^)UjztnB0U?_VN{ Date: Tue, 20 Jan 2026 15:59:31 +0400 Subject: [PATCH 147/154] Updated on 2026-08-14 --- .../tangem/data/pay/DefaultTangemPayEligibilityManager.kt | 3 ++- .../com/tangem/domain/pay/TangemPayEligibilityManager.kt | 1 + .../main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt | 7 ++++++- .../child/wallet/model/intents/TangemPayClickIntents.kt | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) 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 9c27024b35..1f2acd1d9b 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 @@ -44,6 +44,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( override suspend fun getTangemPayAvailability(): Boolean { return onboardingRepository.checkCustomerEligibility() + .also { isEligible -> if (!isEligible) reset() } } private suspend fun getUserWalletsData(): List { @@ -120,7 +121,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } - private fun reset() { + override fun reset() { cachedEligibleWallets = null eligibleWalletsDeferred?.cancel() eligibleWalletsDeferred = null 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 cc26e4832a..fcb41b6984 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 @@ -6,4 +6,5 @@ interface TangemPayEligibilityManager { suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List suspend fun getTangemPayAvailability(): Boolean + fun reset() } \ No newline at end of file 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 ba0446ea8a..f729fe489c 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 @@ -6,6 +6,7 @@ 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.KycStartInfo +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -22,6 +23,7 @@ class DefaultKycModel @Inject constructor( analytics: AnalyticsEventHandler, override val dispatchers: CoroutineDispatcherProvider, private val kycRepository: KycRepository, + private val tangemPayEligibilityManager: TangemPayEligibilityManager, ) : Model() { private val params: KycComponent.Params = paramsContainer.require() @@ -33,7 +35,10 @@ class DefaultKycModel @Inject constructor( analytics.send(TangemPayAnalyticsEvents.KycFlowOpened()) modelScope.launch { try { - kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) } + kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { state -> + _uiState.emit(state) + tangemPayEligibilityManager.reset() + } } catch (e: Exception) { Timber.e(e) } 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 e7a6f9325e..d3f7ede20a 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 @@ -198,6 +198,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( if (isEligible) { router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) } else { + stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId)) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } } From 5b7e1caf7cc2aab660e30b2bfc60a1c99db30f46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Jan 2026 23:08:14 +0400 Subject: [PATCH 148/154] Updated on 2026-08-14 --- .../wallet/child/wallet/model/intents/TangemPayClickIntents.kt | 1 - 1 file changed, 1 deletion(-) 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 d3f7ede20a..1c77b81c07 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 @@ -199,7 +199,6 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) } else { stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId)) - tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } } } From 0447fca1cf6d0dd5e48c619866142bb17a5fcdbc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jan 2026 13:39:10 +0400 Subject: [PATCH 149/154] Updated on 2026-08-14 --- .../visa/TangemPayOnboardingBanner.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 62e59a5eed..d345eede90 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 @@ -51,15 +51,28 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m ) { val (image, text, close) = createRefs() + 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), + ) + Column( modifier = Modifier .constrainAs(text) { top.linkTo(parent.top) start.linkTo(image.end, margin = 12.dp) - end.linkTo(parent.end) + end.linkTo(close.start, margin = 12.dp) width = Dimension.fillToConstraints } - .padding(top = 16.dp, end = 16.dp, bottom = 16.dp), + .padding(top = 16.dp, bottom = 16.dp), ) { Text( text = stringResourceSafe(R.string.tangempay_onboarding_banner_title), @@ -80,26 +93,13 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner), contentDescription = null, modifier = Modifier + .padding(top = 8.dp, start = 24.dp) .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 57134840313dcb6877b4d281ff167f253e539487 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jan 2026 20:40:49 +0300 Subject: [PATCH 150/154] Updated on 2026-08-14 --- .../com/tangem/domain/walletconnect/WcAnalyticEvents.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 27ca88f239..49d787246d 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 @@ -87,7 +87,7 @@ sealed class WcAnalyticEvents( AnalyticsParam.ERROR_CODE to errorCode, AnalyticsParam.ERROR_DESCRIPTION to errorMessage, ), - ), AppsFlyerIncludedEvent + ) class SessionDisconnected(dAppMetaData: WcAppMetaData) : WcAnalyticEvents( event = "dApp Disconnected", @@ -146,7 +146,7 @@ sealed class WcAnalyticEvents( AnalyticsParam.BLOCKCHAIN to network.name, AnalyticsParam.TYPE to securityStatus.toAnalyticVerificationStatus(), ), - ) + ), AppsFlyerIncludedEvent class SignatureRequestFailed( rawRequest: WcSdkSessionRequest, From 39303971df321afdf3f1120765f30bc09b38818a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jan 2026 20:48:25 +0300 Subject: [PATCH 151/154] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-it/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-uk-rUA/strings.xml | 2 +- core/res/src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 6368d1bf58..913619547d 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1554,7 +1554,7 @@ Tangem Pay ist vorübergehend nicht erreichbar. Tangem Pay Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen - Sie können nur USDC Polygon von Ihrer On-Chain-Adresse abheben. Ihr On-Chain-Guthaben unterscheidet sich von Ihrem Kartenguthaben und wird innerhalb von 2 Werktagen nach einem Kauf aktualisiert (bei Rückerstattungen oder stornierten Transaktionen kann es bis zu 14 Werktage dauern). + Ihr USDC Polygon On-Chain-Guthaben unterscheidet sich von Ihrem Kartenguthaben und wird innerhalb von 2 Werktagen nach einem Kauf aktualisiert. Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar. Bitte beachten Sie Ihr PIN-Code Das ist meine Wallet diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 29ba81c3de..a1571141cb 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1554,7 +1554,7 @@ Tangem Pay temporalmente no disponible Tangem Pay Haga clic en el botón de abajo para restaurar el acceso - Solo puedes retirar USDC Polygon disponibles en tu dirección on-chain. El saldo on-chain difiere del saldo de tu tarjeta y se actualiza en 2 días hábiles después de una compra (para reembolsos o transacciones canceladas puede tomar hasta 14 días hábiles). + Tu saldo USDC Polygon on-chain difiere de tu saldo de tarjeta y se actualiza dentro de 2 días hábiles después de una compra. Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras. Tenga en cuenta Tu código PIN Esta es mi billetera diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index f0b5703f1c..b5b5c350eb 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1547,7 +1547,7 @@ Tangem Pay est temporairement indisponible Tangem Pay Cliquez sur le bouton ci-dessous pour restaurer l\'accès - Vous ne pouvez retirer que les USDC Polygon disponibles sur votre adresse on-chain. Le solde on-chain diffère du solde de votre carte et se met à jour dans les 2 jours ouvrables après un achat (pour les remboursements ou transactions annulées, cela peut prendre jusqu\'à 14 jours ouvrables). + Votre solde USDC Polygon on-chain diffère de votre solde de carte et se met à jour dans les 2 jours ouvrables suivant un achat. Les fonds des achats remboursés ne seront pas retournés à votre solde on-chain ni disponibles pour un retrait, mais resteront sur votre solde de carte pour vos achats. Veuillez noter Votre code PIN C\'est mon portefeuille diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index ca85a31ba2..0c8bcde7ac 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -174,7 +174,7 @@ Tangem Pay è temporaneamente non disponibile Tangem Pay Fare clic sul pulsante in basso per ripristinare l\'accesso - Puoi prelevare solo USDC Polygon disponibili sul tuo indirizzo on-chain. Il saldo on-chain differisce dal saldo della carta e si aggiorna entro 2 giorni lavorativi dopo un acquisto (per rimborsi o transazioni annullate può richiedere fino a 14 giorni lavorativi). + Il tuo saldo USDC Polygon on-chain differisce dal saldo della carta e si aggiorna entro 2 giorni lavorativi dopo un acquisto. I fondi degli acquisti rimborsati non verranno restituiti al tuo saldo on-chain né saranno disponibili per il prelievo, ma rimarranno sul saldo della tua carta per gli acquisti. Attenzione Il tuo codice PIN Tangem Twin diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e10e9e29ae..619641f86b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1548,7 +1548,7 @@ Tangem Payは現在一時的に利用できません。 Tangem Pay 下のボタンをクリックしてアクセスを復元してください - オンチェーンアドレスにある USDC Polygon のみ出金可能です。オンチェーン残高はカード残高とは異なり、購入後 2 営業日以内に更新されます(返金またはキャンセルされた取引の場合、最大 14 営業日かかることがあります)。 + USDC Polygonのオンチェーン残高はカード残高とは異なり、購入後2営業日以内に更新されます。返金された購入資金はオンチェーン残高に戻らず、出金もできませんが、カード残高に残り、購入に使用できます。 ご注意ください PINコード これは私のウォレットです diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fceb5bf0dd..3968ee7d53 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1594,7 +1594,7 @@ Tangem Pay временно недоступен Tangem Pay Нажмите на кнопку ниже, чтобы восстановить доступ - Вывод доступен только для USDC Polygon на вашем ончейн-адресе. Баланс ончейн-адреса отличается от баланса карты и обновляется в течение 2 рабочих дней после покупки (при возвратах или отменах транзакций — до 14 рабочих дней). + Баланс ончейн-адреса (USDC Polygon) обновляется в течение 2 рабочих дней после покупки. При возвратах покупок средства не возвращаются на ончейн-баланс и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок. Обратите внимание Ваш 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 bcde67362a..fb203cd320 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1570,7 +1570,7 @@ Tangem Pay тимчасово недоступний Tangem Pay Натисніть кнопку нижче, щоб відновити доступ - Виведення доступне лише для USDC Polygon на вашій ончейн-адресі. Баланс ончейн-адреси відрізняється від балансу картки та оновлюється протягом 2 робочих днів після покупки (у разі повернень або скасувань транзакцій — до 14 робочих днів). + Ваш ончейн-баланс USDC Polygon відрізняється від балансу картки та оновлюється протягом 2 робочих днів після покупки. Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс і не будуть доступні для виведення, але залишаться на балансі картки для покупок. Зверніть увагу Ваш PIN-код Це мій гаманець 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 8f05307960..ad66420637 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -416,7 +416,7 @@ Tangem Pay暂时不可用 Tangem Pay 點擊下方按鈕以恢復存取權限 - 您僅能提領鏈上地址中的 USDC Polygon。鏈上餘額與卡片餘額不同,購買後將在 2 個工作日內更新(若有退款或取消交易,最多需 14 個工作日)。 + 您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。 請注意 您的PIN码 隱藏 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3cf0a8eaec..234bdd266d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1568,7 +1568,7 @@ Tangem Pay is temporarily unreachable Tangem Pay Click the button below to restore access - You can only withdraw USDC Polygon currently available on your on-chain address. Your on-chain balance differs from your card balance and updates within 2 business days after a purchase (for refunds or canceled transactions it may take up to 14 business days). + Your on-chain USDC Polygon balance differs from your card balance and updates within 2 business days after a purchase. Funds from refunded purchases won’t be returned your on-chain balance or be available for withdrawal, but will stay on your card balance for purchases. Please note Your PIN code This is my wallet From 910be115310cb1a2a93d2ce8b1f64d2fa5194c27 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jan 2026 22:13:53 +0400 Subject: [PATCH 152/154] Updated on 2026-08-14 --- .../repository/DefaultTxHistoryRepository.kt | 22 +++++-------------- domain/txhistory/build.gradle.kts | 6 +++++ .../repository/TxHistoryRepository.kt | 5 +---- .../GetExplorerTransactionUrlUseCase.kt | 12 +++++++--- .../v2/send/confirm/model/SendConfirmModel.kt | 1 + .../confirm/model/NFTSendConfirmModel.kt | 1 + .../state/helpers/StakingTransactionSender.kt | 3 ++- .../confirm/model/SendWithSwapConfirmModel.kt | 1 + .../tangem/feature/swap/model/SwapModel.kt | 1 + .../tokendetails/model/TokenDetailsModel.kt | 1 + .../txhistory/model/TxHistoryModel.kt | 1 + .../intents/WalletContentClickIntents.kt | 13 ++++++----- 12 files changed, 38 insertions(+), 29 deletions(-) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index ed640bfe95..85277a038f 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -75,28 +75,18 @@ class DefaultTxHistoryRepository( return pager.flow } - @Deprecated("Replace with getTxExploreUrl [UserWalletId, Network] instead") - override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String { + override fun getTxExploreUrl(txHash: String, networkId: Network.ID, currency: CryptoCurrency): String { val blockchain = networkId.toBlockchain() - return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) { + val txExploreState = when (currency) { + is CryptoCurrency.Token -> blockchain.getTokenExplorerTxUrl(txHash) + else -> blockchain.getExploreTxUrl(txHash) + } + return when (txExploreState) { is TxExploreState.Url -> txExploreState.url is TxExploreState.Unsupported -> "" } } - override suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String { - val blockchain = network.toBlockchain() - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - network = network, - ) - val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() - return when (val txExploreState = blockchain.getExploreTxUrl(lastTxHash)) { - is TxExploreState.Url -> txExploreState.url - else -> "" - } - } - override suspend fun getFixedSizeTxHistoryItems( userWalletId: UserWalletId, currency: CryptoCurrency, diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts index 5654e0148d..4e4655573b 100644 --- a/domain/txhistory/build.gradle.kts +++ b/domain/txhistory/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -23,4 +25,8 @@ dependencies { implementation(deps.androidx.paging.runtime) api(projects.core.pagination) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index d040f021e3..31a250f6f3 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -22,10 +22,7 @@ interface TxHistoryRepository { refresh: Boolean, ): Flow> - fun getTxExploreUrl(txHash: String, networkId: Network.ID): String - - /** Get transaction url in explorer via last transaction from wallet's recentTransactions list */ - suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String + fun getTxExploreUrl(txHash: String, networkId: Network.ID, currency: CryptoCurrency): String @Throws(TxHistoryListError::class) suspend fun getFixedSizeTxHistoryItems( diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index dcbec88d76..4f196ee9e1 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -3,18 +3,24 @@ package com.tangem.domain.txhistory.usecase import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.txhistory.models.TxStatusError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import javax.inject.Inject -class GetExplorerTransactionUrlUseCase( +class GetExplorerTransactionUrlUseCase @Inject constructor( private val repository: TxHistoryRepository, ) { - operator fun invoke(txHash: String, networkId: Network.ID): Either { + operator fun invoke( + txHash: String, + networkId: Network.ID, + currency: CryptoCurrency, + ): Either { return either { catch( block = { - repository.getTxExploreUrl(txHash, networkId).ifEmpty { + repository.getTxExploreUrl(txHash, networkId, currency).ifEmpty { raise(TxStatusError.EmptyUrlError) } }, 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 6fcd04be67..065b3f29ab 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 @@ -460,6 +460,7 @@ internal class SendConfirmModel @Inject constructor( val txUrl = getExplorerTransactionUrlUseCase( txHash = txData.hash.orEmpty(), networkId = cryptoCurrency.network.id, + currency = cryptoCurrency, ).getOrNull().orEmpty() _uiState.update(SendConfirmSentStateTransformer(txData, txUrl)) } 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 8bab99a2a4..548a368a73 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 @@ -335,6 +335,7 @@ internal class NFTSendConfirmModel @Inject constructor( val txUrl = getExplorerTransactionUrlUseCase( txHash = txData.hash.orEmpty(), networkId = cryptoCurrency.network.id, + currency = cryptoCurrency, ).getOrElse { "" } _uiState.update(NFTSendConfirmSentStateTransformer(txData, txUrl)) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index ef5030f334..f05d4cde06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -253,7 +253,8 @@ internal class StakingTransactionSender @AssistedInject constructor( val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, - ).getOrNull() ?: "" + currency = cryptoCurrencyStatus.currency, + ).getOrNull().orEmpty() balanceUpdater.updateAfterTransaction() onSendSuccess(txUrl) 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 3fcd6d870c..993f4ea095 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 @@ -288,6 +288,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = primaryCurrencyStatus.currency.network.id, + currency = primaryCurrencyStatus.currency, ).getOrNull().orEmpty() sendSuccessAnalytics() uiState.transformerUpdate( 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 38d591dd72..65dba91ee9 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 @@ -781,6 +781,7 @@ internal class SwapModel @Inject constructor( val url = getExplorerTransactionUrlUseCase( txHash = swapTransactionState.txHash, networkId = fromCurrency.currency.network.id, + currency = fromCurrency.currency, ).getOrElse { Timber.i("tx hash explore not supported") "" 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 0b39af391f..f1e1c2c815 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 @@ -830,6 +830,7 @@ internal class TokenDetailsModel @Inject constructor( getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrency.network.id, + currency = cryptoCurrency, ).fold( ifLeft = { Timber.e(it.toString()) }, ifRight = { router.openUrl(url = it) }, diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index b5586fdb53..88c8216469 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -222,6 +222,7 @@ internal class TxHistoryModel @Inject constructor( getExplorerTransactionUrlUseCase( txHash = txHash, networkId = params.currency.network.id, + currency = params.currency, ).fold( ifLeft = { Timber.e(it.toString()) }, ifRight = { urlOpener.openUrl(url = it) }, 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 2fa16a3f5f..d301a20f0c 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 @@ -322,11 +322,14 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( ?.currency ?: return@launch - getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id) - .fold( - ifLeft = { Timber.e(it.toString()) }, - ifRight = { router.openUrl(url = it) }, - ) + getExplorerTransactionUrlUseCase( + txHash = txHash, + networkId = currency.network.id, + currency = currency, + ).fold( + ifLeft = { Timber.e(it.toString()) }, + ifRight = { router.openUrl(url = it) }, + ) } } From 1dc34173a8a37936be2ece0a328f8bd311ba7e7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 24 Jan 2026 15:09:31 +0300 Subject: [PATCH 153/154] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 + .../create-wallet-start/impl/build.gradle.kts | 1 + .../CreateWalletStartModel.kt | 3 + .../entity/CreateWalletStartUM.kt | 1 + .../ui/CreateWalletStartContent.kt | 120 +++++++++--------- .../preview/PreviewDetailsComponent.kt | 1 + .../hotwallet/HotWalletFeatureToggles.kt | 1 + .../DefaultHotWalletFeatureToggles.kt | 2 + 8 files changed, 75 insertions(+), 58 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 94775b09a4..c2e3aa55ea 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": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, + { + "name": "HOT_WALLET_VISIBLE", + "version": "undefined" + }, { "name": "TANGEM_PAY_ENABLED", "version": "5.31.0" diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 8552f7b7d9..987ab1a58b 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.createWalletStart.api) + implementation(projects.features.hotWallet.api) /** Project - Domain */ implementation(projects.domain.card) 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 2183fc05a0..0760ef5917 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 @@ -32,6 +32,7 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletstart.entity.CreateWalletStartUM +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay @@ -63,6 +64,7 @@ internal class CreateWalletStartModel @Inject constructor( private val urlOpener: UrlOpener, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -97,6 +99,7 @@ internal class CreateWalletStartModel @Inject constructor( onBackClick = { router.pop() }, onScanClick = ::onScanClick, isScanInProgress = false, + isHotWalletOptionVisible = hotWalletFeatureToggles.isHotWalletVisible, ) CreateWalletStartComponent.Mode.HotWallet -> CreateWalletStartUM( title = resourceReference(R.string.hw_mobile_wallet), 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 bce2d230b2..babf146ab3 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 @@ -17,6 +17,7 @@ internal data class CreateWalletStartUM( val otherMethodClick: () -> Unit, val onScanClick: () -> Unit, val onBackClick: () -> Unit, + val isHotWalletOptionVisible: Boolean = true, ) { data class FeatureItem( val iconResId: Int, 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 e113e454a5..d88ad17317 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 @@ -165,70 +165,74 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi text = state.primaryButtonText.resolveReference(), onClick = state.onPrimaryButtonClick, ) - Row( - modifier = Modifier - .padding( - start = 16.dp, - top = 24.dp, - end = 16.dp, - ), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - DashedGradientLine( + if (state.isHotWalletOptionVisible) { + Row( modifier = Modifier - .weight(1f) - .height(16.dp), - ) - Text( - text = stringResourceSafe(R.string.common_or), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - DashedGradientLine( - modifier = Modifier - .weight(1f) - .height(16.dp) - .scale(scaleX = -1f, scaleY = 1f), - ) - } - if (state.otherMethodDescription != null) { - Text( - modifier = Modifier - .fillMaxWidth() .padding( start = 16.dp, - top = 16.dp, + top = 24.dp, end = 16.dp, ), - text = state.otherMethodDescription.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DashedGradientLine( + modifier = Modifier + .weight(1f) + .height(16.dp), + ) + Text( + text = stringResourceSafe(R.string.common_or), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + DashedGradientLine( + modifier = Modifier + .weight(1f) + .height(16.dp) + .scale(scaleX = -1f, scaleY = 1f), + ) + } } - Row( - modifier = Modifier - .wrapContentWidth() - .clickable { state.otherMethodClick() } - .padding( - horizontal = 16.dp, - vertical = 12.dp, - ), - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = state.otherMethodTitle.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_18x24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) + if (state.isHotWalletOptionVisible) { + if (state.otherMethodDescription != null) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + ), + text = state.otherMethodDescription.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } + Row( + modifier = Modifier + .wrapContentWidth() + .clickable { state.otherMethodClick() } + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = state.otherMethodTitle.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_18x24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } } }, minImageHeight = 160.dp, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 45c7cd73ca..3373105976 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -23,6 +23,7 @@ internal class PreviewDetailsComponent : DetailsComponent { hotWalletFeatureToggles = object : HotWalletFeatureToggles { override val isHotWalletEnabled: Boolean = true override val isWalletCreationRestrictionEnabled: Boolean = true + override val isHotWalletVisible: Boolean = true }, ).buildAll( isWalletConnectAvailable = true, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt index c0526863e1..02cc6b1973 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -3,4 +3,5 @@ package com.tangem.features.hotwallet interface HotWalletFeatureToggles { val isHotWalletEnabled: Boolean val isWalletCreationRestrictionEnabled: Boolean + val isHotWalletVisible: Boolean } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt index b635ab78db..9659ed2c61 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -10,4 +10,6 @@ internal class DefaultHotWalletFeatureToggles( override val isWalletCreationRestrictionEnabled: Boolean get() = isHotWalletEnabled && featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_CREATION_RESTRICTION_ENABLED") + override val isHotWalletVisible: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_VISIBLE") } \ No newline at end of file From 8e16e23add8f7be5b166acf8985e77405a8352f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 24 Jan 2026 16:51:04 +0300 Subject: [PATCH 154/154] 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 370da97987..5802616373 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-1382" +tangemBlockchainSdk = "releases-5.32-1386" #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 ^