From 94c372754b5668e84eb9fa32dd4a1ba1b13f2d53 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 22:10:37 +0500 Subject: [PATCH 01/29] Updated on 2026-08-14 --- .../send/v2/common/PredefinedValues.kt | 2 +- .../send/v2/send/DefaultSendComponent.kt | 18 +---- .../features/send/v2/send/model/SendModel.kt | 76 +++++++++++++------ 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt index ef94394fcf..9ebfa54ccf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt @@ -9,7 +9,7 @@ sealed class PredefinedValues { abstract val memo: String? data class Deeplink( - override val amount: String?, + override val amount: String, override val address: String, override val memo: String?, val transactionId: String, 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 3f2eba602f..36b57ddd7c 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 @@ -21,7 +21,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.SendContent @@ -252,20 +251,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent { val cryptoCurrencyStatus = model.cryptoCurrencyStatus val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus - val predefinedAmount = params.amount - val predefinedTxId = params.transactionId - val predefinedAddress = params.destinationAddress - val predefinedValues = - if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { - PredefinedValues.Content.Deeplink( - amount = predefinedAmount, - address = predefinedAddress, - memo = params.tag, - transactionId = predefinedTxId, - ) - } else { - PredefinedValues.Empty - } + return if (cryptoCurrencyStatus != null && feeCryptoCurrencyStatus != null) { SendConfirmComponent( appComponentContext = factoryContext, @@ -279,7 +265,7 @@ internal class DefaultSendComponent @AssistedInject constructor( feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, appCurrency = model.appCurrency, callback = model, - predefinedValues = predefinedValues, + predefinedValues = model.predefinedValues, onLoadFee = model::loadFee, ), ) 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 ff273f2b85..d8878e70e0 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 @@ -11,6 +11,7 @@ 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.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -111,6 +112,7 @@ internal class SendModel @Inject constructor( subscribeOnQRScannerResult() subscribeOnCurrencyStatusUpdates() initAppCurrency() + initPredefinedValues() } override fun onNavigationResult(navigationUM: NavigationUM) { @@ -135,19 +137,31 @@ internal class SendModel @Inject constructor( } suspend fun loadFee(): Either { - val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") - val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") - val enteredDestinationAddress = destinationUM.addressTextField.value - val enteredMemo = destinationUM.memoTextField?.value - val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") + val predefinedValues = predefinedValues + val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) { + val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()?.convertToSdkAmount(cryptoCurrency) + createTransferTransactionUseCase( + amount = predefinedAmount ?: error("Invalid amount"), + memo = predefinedValues.memo, + destination = predefinedValues.address, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + } else { + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") + val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") + val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredMemo = destinationUM.memoTextField?.value + val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") - val transferTransaction = createTransferTransactionUseCase( - amount = enteredAmount.convertToSdkAmount(cryptoCurrency), - memo = enteredMemo, - destination = enteredDestinationAddress, - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - ).getOrElse { + createTransferTransactionUseCase( + amount = enteredAmount.convertToSdkAmount(cryptoCurrency), + memo = enteredMemo, + destination = enteredDestinationAddress, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + }.getOrElse { return GetFeeError.DataError(it).left() } @@ -158,16 +172,6 @@ internal class SendModel @Inject constructor( ) } - private fun resetPredefinedAmount() { - // reset predefined amount - val internalPredefinedValues = predefinedValues - predefinedValues = when (internalPredefinedValues) { - is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = null) - is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null) - PredefinedValues.Empty -> internalPredefinedValues - } - } - fun showAlertError() { sendConfirmAlertFactory.getGenericErrorState( onFailedTxEmailClick = ::onFailedTxEmailClick, @@ -181,6 +185,34 @@ internal class SendModel @Inject constructor( } } + private fun initPredefinedValues() { + val predefinedAmount = params.amount + val predefinedTxId = params.transactionId + val predefinedAddress = params.destinationAddress + + predefinedValues = if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { + PredefinedValues.Content.Deeplink( + amount = predefinedAmount, + address = predefinedAddress, + memo = params.tag, + transactionId = predefinedTxId, + ) + } else { + PredefinedValues.Empty + } + } + + private fun resetPredefinedAmount() { + // reset predefined amount + val internalPredefinedValues = predefinedValues + predefinedValues = when (internalPredefinedValues) { + is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null) + is PredefinedValues.Content.Deeplink, + PredefinedValues.Empty, + -> internalPredefinedValues + } + } + private fun subscribeOnCurrencyStatusUpdates() { modelScope.launch { getUserWalletUseCase(params.userWalletId).fold( From 8439b9e20fa837052192e18d17c7c1a2e8b04e73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 May 2025 14:20:48 +0400 Subject: [PATCH 02/29] Updated on 2026-08-14 --- .../com/tangem/features/onboarding/v2/common/ui/WalletCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt index 05e2c4fd39..bd7b6d1a54 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt @@ -15,7 +15,7 @@ internal fun WalletCard(artwork: ArtworkUM?, modifier: Modifier = Modifier) { AsyncImage( modifier = modifier, model = ImageRequest.Builder(LocalContext.current) - .data(artwork?.verifiedArtwork ?: artwork?.defaultUrl) + .data(artwork?.verifiedArtwork?.toByteArray() ?: artwork?.defaultUrl) .crossfade(true) .build(), placeholder = painterResource(R.drawable.card_placeholder_black), From 5f91d6077e5e04998e41f2c47c55da67c03e1b60 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:43:37 +0500 Subject: [PATCH 03/29] Updated on 2026-08-14 --- .../ui/expressStatus/ExpressStatusBlock.kt | 5 +-- .../SetOnrampSuccessContentConverter.kt | 45 +++++-------------- ...nDetailsOnrampTransactionStateConverter.kt | 27 +++++------ 3 files changed, 22 insertions(+), 55 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt index ad3c3c1c94..29d0277015 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt @@ -79,10 +79,7 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = resourceReference( - id = R.string.express_transaction_id, - formatArgs = wrappedList(link.text), - ).resolveReference(), + text = link.text.resolveReference(), maxLines = 1, overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.subtitle2, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index ab7e4b6f85..8cc5e95401 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -50,7 +50,6 @@ internal class SetOnrampSuccessContentConverter( statusBlock = convertStatuses( status = value.status, externalTxId = value.externalTxId, - externalTxUrl = value.externalTxUrl, ), notification = getNotification(status = value.status, externalTxUrl = value.externalTxUrl), ) @@ -78,11 +77,7 @@ internal class SetOnrampSuccessContentConverter( null } - private fun convertStatuses( - status: OnrampStatus.Status, - externalTxId: String?, - externalTxUrl: String?, - ): ExpressStatusUM { + private fun convertStatuses(status: OnrampStatus.Status, externalTxId: String?): ExpressStatusUM { val statuses = with(status) { persistentListOf( getAwaitingDepositItem(), @@ -94,7 +89,7 @@ internal class SetOnrampSuccessContentConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status = status, externalTxId = externalTxId, externalTxUrl = externalTxUrl), + link = getStatusLink(externalTxId = externalTxId), statuses = statuses, ) } @@ -192,33 +187,15 @@ internal class SetOnrampSuccessContentConverter( state = getStatusState(OnrampStatus.Status.Sending), ) - private fun getStatusLink( - status: OnrampStatus.Status, - externalTxId: String?, - externalTxUrl: String?, - ): ExpressLinkUM { - if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - goToProviderClick(externalTxUrl) - }, - ) - } - else -> if (externalTxId != null) { - ExpressLinkUM.Content( - icon = R.drawable.ic_copy_24, - text = stringReference(externalTxId), - onClick = { onCopyClick(externalTxId) }, - ) - } else { - ExpressLinkUM.Empty - } + private fun getStatusLink(externalTxId: String?): ExpressLinkUM { + return if (externalTxId != null) { + ExpressLinkUM.Content( + icon = R.drawable.ic_copy_24, + text = resourceReference(R.string.express_transaction_id, wrappedList(externalTxId)), + onClick = { onCopyClick(externalTxId) }, + ) + } else { + ExpressLinkUM.Empty } } 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 44970bac5f..560485952f 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 @@ -148,7 +148,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status, externalTxUrl), + link = getStatusLink(externalTxUrl), statuses = statuses, ) } @@ -263,23 +263,16 @@ internal class TokenDetailsOnrampTransactionStateConverter( }, ) - private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + private fun getStatusLink(externalTxUrl: String?): ExpressLinkUM { if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(externalTxUrl) - }, - ) - } - else -> ExpressLinkUM.Empty - } + return ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + clickIntents.onGoToProviderClick(externalTxUrl) + }, + ) } private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { From 0f9460509506bac66e1bc9b3fbb9080d449b806f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:43:42 +0500 Subject: [PATCH 04/29] Updated on 2026-08-14 --- .../wallet/child/wallet/model/intents/WalletClickIntents.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index e39efc204f..000fb49bf3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -50,6 +51,7 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val onrampStatusFactory: OnrampStatusFactory, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -168,7 +170,7 @@ internal class WalletClickIntents @Inject constructor( modelScope.launch(dispatchers.main) { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) - + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, clickIntents = this@WalletClickIntents, From b9b65263d44e3fe98961581a4faa4c2b3996c8fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:44:10 +0500 Subject: [PATCH 05/29] Updated on 2026-08-14 --- .../features/onramp/main/model/OnrampMainComponentModel.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 12371bbef8..8ff42661ed 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 @@ -94,6 +94,10 @@ internal class OnrampMainComponentModel @Inject constructor( userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) + modelScope.launch { + clearOnrampCacheUseCase() + } + sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() From a72837204da2b0799da5afc1cd7f29b5ef7b21c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:06:35 +0300 Subject: [PATCH 06/29] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 16 ++ .../tap/di/domain/SettingsDomainModule.kt | 8 + .../com/tangem/tap/features/home/HomeModel.kt | 2 +- .../response/TokenMarketListResponse.kt | 24 ++ .../local/preferences/PreferencesKeys.kt | 6 + .../components/notifications/Notification.kt | 2 +- .../img_staking_in_market_notification.webp | Bin 0 -> 15688 bytes data/markets/build.gradle.kts | 1 + .../markets/DefaultMarketsTokenRepository.kt | 13 +- .../converters/TokenListConfigConverters.kt | 1 + .../converters/TokenMarketListConverter.kt | 17 +- .../data/markets/di/MarketsDataModule.kt | 3 +- .../data/promo/DefaultPromoRepository.kt | 14 ++ .../settings/DefaultSettingsRepository.kt | 11 + domain/markets/build.gradle.kts | 2 + .../com/tangem/domain/markets/TokenMarket.kt | 1 + .../domain/markets/TokenMarketListConfig.kt | 2 +- .../markets/TokenMarketListWithMaxApy.kt | 8 + .../GetStakingNotificationMaxApyUseCase.kt | 39 +++ .../repositories/MarketsTokenRepository.kt | 4 + .../tangem/domain/promo/PromoRepository.kt | 4 + .../SetWalletFirstTimeUsageUseCase.kt | 16 ++ .../repositories/SettingsRepository.kt | 4 + .../analytics/MarketsListAnalyticsEvent.kt | 7 + .../tokenlist/impl/model/MarketsListModel.kt | 233 ++++++++++-------- .../converters/MarketsTokenItemConverter.kt | 10 +- .../MarketsListBatchFlowManager.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 22 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 67 ++++- .../impl/ui/components/MarketsListItem.kt | 39 ++- .../StakingInMarketsPromoNotification.kt | 147 +++++++++++ .../MarketChartListItemPreviewDataProvider.kt | 19 +- .../impl/ui/state/MarketsListItemUM.kt | 4 +- .../tokenlist/impl/ui/state/MarketsListUM.kt | 5 + .../wallet/child/wallet/model/WalletModel.kt | 8 + 35 files changed, 628 insertions(+), 132 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_staking_in_market_notification.webp create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.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 7666729a36..4889626ff1 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 @@ -5,8 +5,10 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -109,4 +111,18 @@ object MarketsDomainModule { fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase { return GetTokenExchangesUseCase(marketsTokenRepository = marketsTokenRepository) } + + @Provides + @Singleton + fun provideGetStakingNotificationMaxApyUseCase( + settingsRepository: SettingsRepository, + promoRepository: PromoRepository, + marketsTokenRepository: MarketsTokenRepository, + ): GetStakingNotificationMaxApyUseCase { + return GetStakingNotificationMaxApyUseCase( + settingsRepository = settingsRepository, + promoRepository = promoRepository, + marketsTokenRepository = marketsTokenRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 7dc5634128..df25dd1255 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -226,5 +226,13 @@ internal object SettingsDomainModule { fun provideIsGooglePayAvailableUseCase(settingsRepository: SettingsRepository): IsGooglePayAvailableUseCase { return IsGooglePayAvailableUseCase(settingsRepository) } + + @Provides + @Singleton + fun provideMaybeSetWalletFirstTimeUsageUseCase( + settingsRepository: SettingsRepository, + ): SetWalletFirstTimeUsageUseCase { + return SetWalletFirstTimeUsageUseCase(settingsRepository) + } // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 91c45b832a..a3b184dc8e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -49,12 +49,12 @@ internal class HomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val getUserCountryUseCase: GetUserCountryUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val userWalletBuilderFactory: UserWalletBuilder.Factory, + getUserCountryUseCase: GetUserCountryUseCase, ) : Model() { private val tangemErrorHandler = TangemTangemErrorsHandler(store) 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 b2af512cda..3c1829900b 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 @@ -12,6 +12,7 @@ data class TokenMarketListResponse( @Json(name = "limit") val limit: Int, @Json(name = "offset") val offset: Int, @Json(name = "timestamp") val timestamp: Long? = null, + @Json(name = "summary") val summary: Summary? = null, ) { @JsonClass(generateAdapter = true) @@ -24,6 +25,7 @@ data class TokenMarketListResponse( @Json(name = "market_rating") val marketRating: Int?, @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, + @Json(name = "staking_opportunities") val stakingOpportunities: List?, ) { @JsonClass(generateAdapter = true) @@ -32,5 +34,27 @@ data class TokenMarketListResponse( @Json(name = "1w") val week1: BigDecimal?, @Json(name = "30d") val day30: BigDecimal?, ) + + @JsonClass(generateAdapter = true) + data class StakingOpportunities( + @Json(name = "id") val id: Int?, + @Json(name = "apy") val apy: BigDecimal?, + @Json(name = "network_id") val networkId: String?, + @Json(name = "reward_type") val rewardType: RewardType?, + ) + + @JsonClass(generateAdapter = false) + enum class RewardType { + @Json(name = "apy") APY, + + @Json(name = "apr") APR, + + UNKNOWN, + } } + + @JsonClass(generateAdapter = true) + data class Summary( + @Json(name = "max_apy") val maxApy: BigDecimal?, + ) } \ 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 90b07d055f..5e4f9577d1 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 @@ -86,6 +86,12 @@ 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 WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") } + val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } 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 4050ff267f..a22a0c468c 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 @@ -321,7 +321,7 @@ private fun SecondaryPairButtons( } @Composable -internal fun CloseableIconButton( +fun CloseableIconButton( onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true, diff --git a/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp b/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp new file mode 100644 index 0000000000000000000000000000000000000000..de2b304a0f2fe8eaf61d490c5108bcbb91380b91 GIT binary patch literal 15688 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 literal 0 HcmV?d00001 diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index fa746e853f..dec987eadb 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { // endregion // region Others dependencies + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.jodatime) implementation(deps.moshi) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 91c229e3ef..3eb641be45 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -27,7 +27,9 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import java.math.BigDecimal import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -41,6 +43,7 @@ internal class DefaultMarketsTokenRepository( private val excludedBlockchains: ExcludedBlockchains, private val cacheRegistry: CacheRegistry, private val tokenExchangesStore: RuntimeStateStore>, + private val maxApyStore: RuntimeStateStore, ) : MarketsTokenRepository { private val tokenMarketInfoConverter: TokenMarketInfoConverter = TokenMarketInfoConverter(excludedBlockchains) @@ -88,8 +91,12 @@ internal class DefaultMarketsTokenRepository( val last = res.tokens.size < request.limit + val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res) + + maxApyStore.store(tokenMarketListWithMaxApy.maxApy) + return BatchFetchResult.Success( - data = TokenMarketListConverter.convert(res), + data = tokenMarketListWithMaxApy.tokens, last = last, empty = res.tokens.isEmpty(), ) @@ -275,6 +282,10 @@ internal class DefaultMarketsTokenRepository( } } + override suspend fun getMaxApy(): Flow { + return maxApyStore.get() + } + inline fun catchListErrorAndSendEvent(block: () -> T): T { return catchErrorAndSendEvent(block, ::createListErrorEvent) } 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 1875a5b609..e280e160e1 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 @@ -15,6 +15,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) TokenMarketListConfig.Order.Buyers -> "buyers" TokenMarketListConfig.Order.TopGainers -> "gainers" TokenMarketListConfig.Order.TopLosers -> "losers" + TokenMarketListConfig.Order.Staking -> "staking" } 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 2091fcf399..da25a28d09 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 @@ -2,22 +2,29 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListWithMaxApy import com.tangem.domain.markets.TokenQuotesShort import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isPositive -internal object TokenMarketListConverter : Converter> { +internal object TokenMarketListConverter : Converter { - override fun convert(value: TokenMarketListResponse): List { + override fun convert(value: TokenMarketListResponse): TokenMarketListWithMaxApy { val imageHost = value.imageHost ?: run { if (value.tokens.isEmpty()) { - return emptyList() + return TokenMarketListWithMaxApy(emptyList(), null) } else { error("imageHost cannot be null") } } - return value.tokens.map { token -> + val tokens = value.tokens.map { token -> + val stakingRate = token.stakingOpportunities + ?.mapNotNull { it.apy } + ?.max() + .takeIf { it?.isPositive() == true } + TokenMarket( id = CryptoCurrency.RawID(token.id), name = token.name, @@ -33,7 +40,9 @@ internal object TokenMarketListConverter : Converter { + return appPreferencesStore.get( + key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + default = false, + ) + } + + override suspend fun setMarketsStakingNotificationHideClicked() { + appPreferencesStore.store( + key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + value = true, + ) + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } 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 87ea3f6639..8bc077f7c7 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 @@ -96,6 +96,17 @@ internal class DefaultSettingsRepository( } } + override suspend fun getWalletFirstUsageDate(): Long { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY, + default = 0L, + ) + } + + override suspend fun setWalletFirstUsageDate(value: Long) { + appPreferencesStore.store(key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY, value = value) + } + override suspend fun shouldShowMarketsTooltip(): Boolean { return appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 7d820cb19a..735657a25b 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -23,9 +23,11 @@ dependencies { api(projects.domain.quotes) api(projects.domain.wallets) api(projects.domain.wallets.models) + api(projects.domain.promo) implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) + implementation(projects.domain.settings) api(projects.core.pagination) 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 1724d9ce45..d3c97c6943 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,6 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, + val stakingRate: BigDecimal?, 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 c3633f3925..011df2c630 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 + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking } enum class Interval { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt new file mode 100644 index 0000000000..3b3bc77831 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarketListWithMaxApy( + val tokens: List, + val maxApy: BigDecimal?, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..a14d6c7cbd --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt @@ -0,0 +1,39 @@ +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/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 80a1a97480..c0fe937411 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 @@ -3,6 +3,8 @@ package com.tangem.domain.markets.repositories import com.tangem.domain.markets.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal interface MarketsTokenRepository { @@ -51,4 +53,6 @@ interface MarketsTokenRepository { * @param tokenId token id */ suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List + + suspend fun getMaxApy(): Flow } \ 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 88682deae2..bed593a152 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 @@ -15,6 +15,10 @@ interface PromoRepository { suspend fun setNeverToShowWalletPromo(promoId: PromoId) suspend fun setNeverToShowTokenPromo(promoId: PromoId) + + suspend fun isMarketsStakingNotificationHideClicked(): Flow + + suspend fun setMarketsStakingNotificationHideClicked() // endregion // region Stories diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt new file mode 100644 index 0000000000..c0cef31d77 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.SettingsRepository +import java.util.Calendar + +class SetWalletFirstTimeUsageUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke() = Either.catch { + val savedTime = settingsRepository.getWalletFirstUsageDate() + + if (savedTime == 0L) { + settingsRepository.setWalletFirstUsageDate(Calendar.getInstance().timeInMillis) + } + } +} \ 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 23b4a1756c..1a64989894 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 @@ -30,6 +30,10 @@ interface SettingsRepository { suspend fun incrementAppLaunchCounter() + suspend fun getWalletFirstUsageDate(): Long + + suspend fun setWalletFirstUsageDate(value: Long) + suspend fun shouldShowMarketsTooltip(): Boolean suspend fun setMarketsTooltipShown(value: Boolean) 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 99f0faa3c3..3395db941b 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 @@ -23,6 +23,7 @@ internal sealed class MarketsListAnalyticsEvent( SortByTypeUM.ExperiencedBuyers -> "Buyers" SortByTypeUM.TopGainers -> "Gainers" SortByTypeUM.TopLosers -> "Losers" + SortByTypeUM.Staking -> "Staking" }, "Period" to when (interval) { MarketsListUM.TrendInterval.H24 -> "24h" @@ -31,4 +32,10 @@ internal sealed class MarketsListAnalyticsEvent( }, ), ) + + data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + + data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") + + data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") } \ 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 65622d2f0d..c3c73fed42 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 @@ -1,6 +1,7 @@ package com.tangem.features.markets.tokenlist.impl.model import androidx.compose.runtime.Stable +import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -8,7 +9,13 @@ import com.tangem.core.decompose.model.Model 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.TokenMarket +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.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent @@ -21,8 +28,10 @@ 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.coroutines.* import kotlinx.coroutines.flow.* +import java.math.BigDecimal import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -31,23 +40,26 @@ private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @ModelScoped @Stable +@Suppress("LongParameterList") internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + private val promoRepository: PromoRepository, + private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private var updateQuotesJob = JobHolder() - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) private val visibleItemIds = MutableStateFlow>(emptyList()) @@ -57,7 +69,10 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, + onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, + onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, ) + private val mainMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -92,31 +107,60 @@ internal class MarketsListModel @Inject constructor( val state = marketsListUMStateManager.state.asStateFlow() init { - @Suppress("UnnecessaryParentheses") - modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .flatMapLatest { isInSearchMode -> - if (isInSearchMode) { - combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - ) { items, isError, notFound -> - (items to isError) to notFound - } - } else { - combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - ) { items, isError -> (items to isError) to false } + @Suppress("UnnecessaryParentheses") modelScope.launch { + marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> + if (isInSearchMode) { + combine( + searchMarketsListManager.uiItems, + searchMarketsListManager.isInInitialLoadingErrorState, + searchMarketsListManager.isSearchNotFoundState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = isSearchNotFoundState, + stakingNotificationMaxApy = stakingMaxApy, + userCountry = userCountry, + ) + } + } else { + combine( + mainMarketsListManager.uiItems, + mainMarketsListManager.isInInitialLoadingErrorState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = false, + stakingNotificationMaxApy = stakingNotificationMaxApy, + userCountry = userCountry, + ) } - }.collect { - marketsListUMStateManager.onUiItemsChanged( - uiItems = it.first.first, - isInErrorState = it.first.second, - isSearchNotFound = it.second, - ) } + }.collect { marketsItemsData -> + val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + } + + if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && + stakingNotificationMaxApy != null + ) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + } + + marketsListUMStateManager.onUiItemsChanged( + uiItems = marketsItemsData.items, + isInErrorState = marketsItemsData.isInErrorState, + isSearchNotFound = marketsItemsData.isSearchNotFound, + stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + }, + ) + } } state.onEach { @@ -126,29 +170,22 @@ internal class MarketsListModel @Inject constructor( }.launchIn(modelScope) // update all lists when user's currency has changed - currentAppCurrency - .drop(1) - .onEach { - mainMarketsListManager.reload() - if (marketsListUMStateManager.isInSearchState) { - searchMarketsListManager.reload() - } - }.launchIn(modelScope) + currentAppCurrency.drop(1).onEach { + mainMarketsListManager.reload() + if (marketsListUMStateManager.isInSearchState) { + searchMarketsListManager.reload() + } + }.launchIn(modelScope) // load charts when new batch is being loaded - mainMarketsListManager.onLastBatchLoadedSuccess - .onEach { - mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } - .launchIn(modelScope) + mainMarketsListManager.onLastBatchLoadedSuccess.onEach { + mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + }.launchIn(modelScope) // listen currently selected interval, update charts if sorting=rating, or reload all list modelScope.launch(dispatchers.default) { - marketsListUMStateManager.state - .map { it.selectedInterval } - .distinctUntilChanged() - .drop(1) + marketsListUMStateManager.state.map { it.selectedInterval }.distinctUntilChanged().drop(1) .collectLatest { interval -> when (marketsListUMStateManager.selectedSortByType) { SortByTypeUM.Rating -> { @@ -163,66 +200,52 @@ internal class MarketsListModel @Inject constructor( // reload list when sorting type has changed modelScope.launch { - marketsListUMStateManager.state - .map { it.selectedSortBy } - .distinctUntilChanged() - .drop(1) - .collectLatest { - mainMarketsListManager.reload() - } + marketsListUMStateManager.state.map { it.selectedSortBy }.distinctUntilChanged().drop(1).collectLatest { + mainMarketsListManager.reload() + } } // listen current visible batch and update charts modelScope.launch { - visibleItemIds - .mapNotNull { - if (it.isNotEmpty()) { - activeListManager.getBatchKeysByItemIds(visibleItemIds.value) - } else { - null - } - } - .distinctUntilChanged() - .collectLatest { visibleBatchKeys -> - // TODO load batch on scroll heat area - activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + visibleItemIds.mapNotNull { + if (it.isNotEmpty()) { + activeListManager.getBatchKeysByItemIds(visibleItemIds.value) + } else { + null } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + // TODO load batch on scroll heat area + activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + } } // ===Search=== modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .collectLatest { isInSearchMode -> - activeListManager = if (isInSearchMode) { - searchMarketsListManager - } else { - searchMarketsListManager.clearStateAndStopAllActions() - mainMarketsListManager - } + marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode -> + activeListManager = if (isInSearchMode) { + searchMarketsListManager + } else { + searchMarketsListManager.clearStateAndStopAllActions() + mainMarketsListManager } + } } modelScope.launch { - marketsListUMStateManager.searchQueryFlow - .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) - .distinctUntilChanged() - .onEach { + marketsListUMStateManager.searchQueryFlow.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) + .distinctUntilChanged().onEach { if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions() - } - .filter { it.isNotEmpty() && activeListManager == searchMarketsListManager } - .collectLatest { + }.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }.collectLatest { searchMarketsListManager.reload(searchText = it) } } modelScope.launch { - searchMarketsListManager - .onLastBatchLoadedSuccess - .collectLatest { - searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } + searchMarketsListManager.onLastBatchLoadedSuccess.collectLatest { + searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + } } // analytics @@ -233,17 +256,14 @@ internal class MarketsListModel @Inject constructor( } private fun initAnalytics() { - containerBottomSheetState - .onEach { - if (it == BottomSheetState.EXPANDED) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) - } - }.launchIn(modelScope) + containerBottomSheetState.onEach { + if (it == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + } + }.launchIn(modelScope) - state - .filter { it.isInSearchMode.not() } - .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) } - .distinctUntilChanged() + state.filter { it.isInSearchMode.not() } + .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged() .onEach { analyticsEventHandler.send(it) }.launchIn(modelScope) @@ -270,4 +290,19 @@ internal class MarketsListModel @Inject constructor( } }.saveIn(updateQuotesJob) } + + private fun onStakingNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + modelScope.launch { + promoRepository.setMarketsStakingNotificationHideClicked() + } + } + + private class MarketsItemsData( + val items: ImmutableList, + val isInErrorState: Boolean, + val isSearchNotFound: Boolean, + val stakingNotificationMaxApy: BigDecimal?, + 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/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c4adb37979..d7352826b5 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 @@ -5,6 +5,8 @@ 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.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.compact import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -12,6 +14,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.utils.converter.Converter @@ -37,8 +40,11 @@ internal class MarketsTokenItemConverter( price = value.getCurrentPrice(), trendPercentText = value.getTrendPercent(), trendType = value.getTrendType(), - chardData = value.getChartData(), + chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.stakingRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, ) } @@ -64,7 +70,7 @@ internal class MarketsTokenItemConverter( prevUI.trendPercentText, ) { new.getTrendPercent() }, trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, - chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, ) } 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 e178484e78..3cae93db38 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 @@ -323,6 +323,7 @@ internal class MarketsListBatchFlowManager( SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers + SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking } } 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 aa7f006d19..ac022dcd2a 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 @@ -16,14 +16,18 @@ 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") internal class MarketsListUMStateManager( private val currentVisibleIds: Provider>, private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, + private val onStakingNotificationClick: () -> Unit, + private val onStakingNotificationCloseClick: () -> Unit, ) { private var sortByBottomSheetIsShown @@ -87,6 +91,7 @@ internal class MarketsListUMStateManager( isInErrorState: Boolean, isSearchNotFound: Boolean, uiItems: ImmutableList, + stakingNotificationMaxApy: BigDecimal?, ) { state.update { when { @@ -102,13 +107,19 @@ internal class MarketsListUMStateManager( it.copy(list = ListUM.Loading) } else -> { - it.updateItems(newItems = uiItems) + it.updateItems( + newItems = uiItems, + stakingNotificationMaxApy = stakingNotificationMaxApy, + ) } } } } - private fun MarketsListUM.updateItems(newItems: ImmutableList): MarketsListUM { + private fun MarketsListUM.updateItems( + newItems: ImmutableList, + stakingNotificationMaxApy: BigDecimal?, + ): MarketsListUM { val currentState = this if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) { @@ -119,6 +130,7 @@ internal class MarketsListUMStateManager( .copy( showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), ), + stakingNotificationMaxApy = stakingNotificationMaxApy, ) } @@ -211,6 +223,12 @@ internal class MarketsListUMStateManager( onOptionClicked = ::onBottomSheetOptionClicked, ), ), + stakingNotificationMaxApy = null, + onStakingNotificationClick = { + onStakingNotificationClick() + selectedSortByType = SortByTypeUM.Staking + }, + onStakingNotificationCloseClick = onStakingNotificationCloseClick, ) 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 e1b6bfd37b..1442d9643f 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.buildAnnotatedString import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 @@ -29,10 +30,11 @@ 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.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +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 @@ -41,6 +43,7 @@ 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 +import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification 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 @@ -48,6 +51,9 @@ 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" @Composable internal fun MarketsList( @@ -110,14 +116,50 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi SpacerH12() } } - AnimatedVisibility(state.isInSearchMode.not()) { - Options( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) + Column { + AnimatedVisibility(state.isInSearchMode.not()) { + Options( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + + AnimatedVisibility( + state.isInSearchMode.not() && + state.stakingNotificationMaxApy != null && + state.selectedSortBy != SortByTypeUM.Staking, + ) { + val showMore = stringResourceSafe(R.string.markets_staking_banner_description_show_more) + val description = stringResourceSafe( + R.string.markets_staking_banner_description_placeholder, + showMore, + ) + + val clickableDescription = buildAnnotatedString { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + 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), + ) + } } } val strokeWidth = TangemTheme.dimens.size0_5 @@ -326,6 +368,9 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), + stakingNotificationMaxApy = BigDecimal(0.12345), + onStakingNotificationClick = {}, + onStakingNotificationCloseClick = {}, ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index c153024da0..4c8ed29713 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -22,6 +23,8 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -95,6 +98,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier .alignByBaseline(), ratingPosition = model.ratingPosition, marketCap = model.marketCap, + stakingRate = model.stakingRate, ) PriceChangeInPercent( modifier = Modifier.alignByBaseline(), @@ -110,7 +114,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier Chart( chartType = model.chartType, - chartRawData = model.chardData, + chartRawData = model.chartData, ) } } @@ -142,7 +146,12 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier } @Composable -private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) { +private fun TokenSubtitle( + ratingPosition: String?, + marketCap: String?, + stakingRate: TextReference?, + modifier: Modifier = Modifier, +) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, @@ -150,6 +159,10 @@ private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: TokenRatingPlace(ratingPosition = ratingPosition) SpacerW4() TokenMarketCapText(text = marketCap ?: "") + if (stakingRate != null) { + SpacerW4() + StakingRate(stakingRate = stakingRate.resolveReference()) + } } } @@ -174,6 +187,28 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) { } } +@Composable +private fun RowScope.StakingRate(stakingRate: String) { + Box( + modifier = Modifier + .alignByBaseline() + .heightIn(min = TangemTheme.dimens.size16) + .border( + width = TangemTheme.dimens.size1, + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersSmall2, + ) + .padding(horizontal = TangemTheme.dimens.spacing5), + ) { + Text( + text = stakingRate, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + @Composable private fun RowScope.TokenMarketCapText(text: String) { Text( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt new file mode 100644 index 0000000000..9026a0ebb4 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.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(0x1F8CD9FF) +private val borderColor = Color(0x3D8CD9FF) + +@Composable +fun StakingInMarketsPromoNotification(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(12.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .requiredWidth(56.dp) + .wrapContentHeight(Alignment.Top, 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 = 76.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 { + StakingInMarketsPromoNotification( + config = NotificationConfig( + title = stringReference("Earn up to 14% APY"), + subtitle = stringReference("Staking is the easiest way to earn rewards on your crypto. Show more"), + iconResId = R.drawable.img_staking_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/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 97eecee6d1..464335eb3b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -4,6 +4,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf @@ -20,10 +21,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -35,8 +37,9 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.NEUTRAL, - chardData = null, + chartData = null, isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -48,10 +51,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.DOWN, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -63,10 +67,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -78,10 +83,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -93,10 +99,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), ), ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 458a1e29a3..2cb553987a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency @Immutable @@ -17,8 +18,9 @@ data class MarketsListItemUM( val price: Price, val trendPercentText: String, val trendType: PriceChangeType, - val chardData: MarketChartRawData?, + val chartData: MarketChartRawData?, val isUnder100kMarketCap: Boolean, + val stakingRate: TextReference?, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP -> MarketChartLook.Type.Growing 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 298f90f22f..2e99a45aec 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 @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal internal data class MarketsListUM( val list: ListUM, @@ -18,6 +19,9 @@ internal data class MarketsListUM( val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, + val stakingNotificationMaxApy: BigDecimal?, + val onStakingNotificationClick: () -> Unit, + val onStakingNotificationCloseClick: () -> Unit, ) { val isInSearchMode get() = searchBar.isActive @@ -35,6 +39,7 @@ enum class SortByTypeUM(val text: TextReference) { ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)), 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)), } @Immutable 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 4922cf66c1..b06d42aa58 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 @@ -67,6 +67,7 @@ internal class WalletModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, + private val setWalletFirstTimeUsageUseCase: SetWalletFirstTimeUsageUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -103,6 +104,7 @@ internal class WalletModel @Inject constructor( suggestToOpenMarkets() maybeMigrateNames() + maybeSetWalletFirstTimeUsage() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() @@ -117,6 +119,12 @@ internal class WalletModel @Inject constructor( } } + private fun maybeSetWalletFirstTimeUsage() { + modelScope.launch { + setWalletFirstTimeUsageUseCase() + } + } + override fun onDestroy() { super.onDestroy() From 96b4a464624802d7f1b9f24ffd8fb96d6b2b8971 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 11:39:28 +0300 Subject: [PATCH 07/29] Updated on 2026-08-14 --- .../api/common/response/ApiResponseExt.kt | 9 +++++++++ .../datasource/api/tangemTech/TangemTechApi.kt | 4 ++-- .../markets/DefaultMarketsTokenRepository.kt | 2 +- .../data/markets/MarketsBatchUpdateFetcher.kt | 16 ++++------------ .../referral/data/ReferralRepositoryImpl.kt | 5 +++-- .../feature/referral/analytics/ReferralEvents.kt | 11 ++++++----- .../feature/referral/model/ReferralModel.kt | 5 ++++- .../referral/models/ReferralStateHolder.kt | 2 +- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt index 9fc9554c4d..0caae8ba1a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt @@ -10,4 +10,13 @@ fun ApiResponse.fold(onSuccess: (T) -> R, onError: (ApiRes is ApiResponse.Error -> onError(cause) is ApiResponse.Success -> onSuccess(data) } +} + +inline fun catchApiResponseError(onError: (ApiResponseError) -> Unit, block: () -> T): T { + return try { + block() + } catch (e: ApiResponseError) { + onError(e) + throw e + } } \ No newline at end of file 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 7b474dd442..48a8dfff98 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 @@ -55,11 +55,11 @@ interface TangemTechApi { /** Returns referral status by [walletId] */ @GET("referral/{walletId}") - suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse + suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse /** Make user referral, requires [StartReferralBody] */ @POST("referral") - suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse + suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse @GET("quotes") suspend fun getQuotes( diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 3eb641be45..c06cb808da 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -113,7 +113,7 @@ internal class DefaultMarketsTokenRepository( tangemTechApi = tangemTechApi, marketsApi = marketsApi, analyticsEventHandler = analyticsEventHandler, - onApiError = { + onApiResponseError = { analyticsEventHandler.send(createListErrorEvent(it).toEvent()) }, ) diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 6a9952d71f..3b5e662379 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -7,6 +7,7 @@ import com.tangem.data.markets.converters.TokenMarketChartsConverter import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.toRequestParam import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.catchApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse @@ -25,7 +26,7 @@ internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, private val analyticsEventHandler: AnalyticsEventHandler, - private val onApiError: (ApiResponseError) -> Unit, + private val onApiResponseError: (ApiResponseError) -> Unit, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( @@ -41,7 +42,7 @@ internal class MarketsBatchUpdateFetcher( val updateTasks = idsToUpdate.map { batchIds -> async { retryOnError { - catchApiError(onApiError) { + catchApiResponseError(onApiResponseError) { marketsApi.getCoinsListCharts( coinIds = batchIds.second.joinToString(separator = ","), interval = updateRequest.interval.toRequestParam(), @@ -71,7 +72,7 @@ internal class MarketsBatchUpdateFetcher( } is TokenMarketUpdateRequest.UpdateQuotes -> { val quotesRes = retryOnError { - catchApiError(onApiError) { + catchApiResponseError(onApiResponseError) { tangemTechApi.getQuotes( currencyId = updateRequest.currencyId, coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), @@ -133,13 +134,4 @@ internal class MarketsBatchUpdateFetcher( } } } - - private inline fun catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T { - return try { - block() - } catch (e: ApiResponseError) { - onError(e) - throw e - } - } } \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 279d0632bf..39650996ba 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -38,7 +39,7 @@ internal class ReferralRepositoryImpl @Inject constructor( val referralData = referralConverter.convert( referralApi.getReferralStatus( walletId = walletId, - ), + ).getOrThrow(), ) referralStatus[walletId] = referralData @@ -66,7 +67,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenId = tokenId, address = address, ), - ), + ).getOrThrow(), ) referralStatus[walletId] = referralData referralData 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 a5d37a1b19..20a2fe5ecd 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,11 +4,12 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { - object ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") - object ClickParticipate : ReferralEvents(event = "Button - Participate") - object ClickCopy : ReferralEvents(event = "Button - Copy") - object ClickShare : ReferralEvents(event = "Button - Share") - object ClickTaC : ReferralEvents(event = "Link - TaC") + 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") } 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 a2379d3f8a..943c75b40d 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 @@ -91,7 +91,10 @@ internal class ReferralModel @Inject constructor( uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { runCatching { referralInteractor.startReferral(params.userWalletId) } - .onSuccess(::showContent) + .onSuccess { + analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) + showContent(it) + } .onFailure { throwable -> if (throwable is ReferralError.UserCancelledException) { lastReferralData?.let { referralData -> diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 2029309da1..77bf7443e4 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -39,7 +39,7 @@ internal data class ReferralStateHolder( val onParticipateClicked: () -> Unit, ) : ReferralInfoState, ReferralInfoContentState - object Loading : ReferralInfoState + data object Loading : ReferralInfoState } data class ErrorSnackbar( From 56c4545d8376520691e32a0608553e779dc0ab04 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 14:29:29 +0300 Subject: [PATCH 08/29] Updated on 2026-08-14 --- .../analytics/MarketsListAnalyticsEvent.kt | 9 +++++++++ .../tokenlist/impl/model/MarketsListModel.kt | 11 +++++++++++ .../MarketsListBatchFlowManager.kt | 19 +++++++++++++++++++ .../statemanager/MarketsListUMStateManager.kt | 2 ++ 4 files changed, 41 insertions(+) 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 3395db941b..f1916f48bb 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 @@ -38,4 +38,13 @@ internal sealed class MarketsListAnalyticsEvent( data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + + data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( + event = "Token Searched", + params = mapOf( + "Result" to if (tokenFound) "Yes" else "No", + ), + ) + + data object 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 c3c73fed42..ed6bdd1c10 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 @@ -71,6 +71,7 @@ internal class MarketsListModel @Inject constructor( onTokenClick = { onTokenUIClicked(it) }, onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, + onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) }, ) private val mainMarketsListManager = MarketsListBatchFlowManager( @@ -248,6 +249,16 @@ internal class MarketsListModel @Inject constructor( } } + searchMarketsListManager.isSearchNotFoundState.onEach { + if (it) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) + } + }.launchIn(modelScope) + + searchMarketsListManager.onFirstBatchLoadedSuccess.onEach { + analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = true)) + }.launchIn(modelScope) + // analytics initAnalytics() 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 3cae93db38..e0c50a234d 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 @@ -84,6 +84,25 @@ internal class MarketsListBatchFlowManager( } } + val onFirstBatchLoadedSuccess = batchFlow.state + .distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size } + .mapNotNull { + 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 + } + } + .filter { it } + val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() 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 ac022dcd2a..3137470f46 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 @@ -28,6 +28,7 @@ internal class MarketsListUMStateManager( private val onTokenClick: (MarketsListItemUM) -> Unit, private val onStakingNotificationClick: () -> Unit, private val onStakingNotificationCloseClick: () -> Unit, + private val onShowTokensUnder100kClicked: () -> Unit, ) { private var sortByBottomSheetIsShown @@ -148,6 +149,7 @@ internal class MarketsListUMStateManager( showUnder100kTokensNotificationWasHidden = false, showUnder100kTokensNotification = true, onShowTokensUnder100kClicked = { + onShowTokensUnder100kClicked() state.update { s -> (s.list as? ListUM.Content)?.let { s.copy( From aa228db11e3ea0f8f5b5ec0419094d2e04957ba9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 17:04:07 +0500 Subject: [PATCH 09/29] Updated on 2026-08-14 --- .../send/v2/subcomponents/fee/model/SendFeeModel.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 5bc3264f89..570cb197f7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -159,8 +159,6 @@ internal class SendFeeModel @Inject constructor( saveResult() } - } else { - saveResult() } } @@ -198,6 +196,7 @@ internal class SendFeeModel @Inject constructor( ) val params = params as? SendFeeComponentParams.FeeParams ?: return params.callback.onFeeResult(uiState.value) + params.onNextClick() } private fun checkLoadFee() { @@ -272,10 +271,7 @@ internal class SendFeeModel @Inject constructor( primaryButton = ButtonsUM.PrimaryButtonUM( text = resourceReference(R.string.common_continue), isEnabled = state.isPrimaryButtonEnabled, - onClick = { - onNextClick() - params.onNextClick() - }, + onClick = ::onNextClick, ), prevButton = null, secondaryPairButtonsUM = null, From f0c65fafdbdbfd92a91ecb85e72d404c075de0f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 18:05:37 +0500 Subject: [PATCH 10/29] Updated on 2026-08-14 --- .../tangem/features/send/v2/common/ui/SendNavigationButtons.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt index 70d852dff8..4c50af08d0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt @@ -113,7 +113,7 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo iconResId = wrappedPairButtonsUM.leftIconResId!!, onClick = { singleEvent { - wrappedPairButtonsUM.onLeftClick + wrappedPairButtonsUM.onLeftClick() } }, modifier = Modifier.weight(1f), From f6905f2604a7fc86a75ac3994f4733275a409c0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 10:58:38 +0300 Subject: [PATCH 11/29] 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 4a9a86f746..39dae33c01 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 @@ -5,7 +5,7 @@ }, { "name": "ONRAMP_ENABLED", - "version": "undefined" + "version": "5.24.0" }, { "name": "VISA_ONBOARDING_ENABLED", From 139ca0598566c49067eeb458c1cb09b8f8059014 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 16:36:58 +0500 Subject: [PATCH 12/29] Updated on 2026-08-14 --- .../tangem/feature/wallet/child/wallet/model/WalletModel.kt | 3 --- 1 file changed, 3 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 b06d42aa58..250b05849d 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 @@ -92,7 +92,6 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() - private val expressStatusJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -253,7 +252,6 @@ internal class WalletModel @Inject constructor( screenLifecycleProvider.isBackgroundState .onEach { isBackground -> expressTxStatusTaskScheduler.cancelTask() - expressStatusJobHolder.cancel() refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() @@ -265,7 +263,6 @@ internal class WalletModel @Inject constructor( } } .launchIn(modelScope) - .saveIn(expressStatusJobHolder) } private fun subscribeOnExpressTransactionsUpdates(userWallet: UserWallet) { From 349d8fc7ddd63b880f0b9dc4c8c294b612b6d83f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 16:37:06 +0500 Subject: [PATCH 13/29] Updated on 2026-08-14 --- .../DefaultOnrampTransactionRepository.kt | 21 ++++++++++------ .../OnrampUpdateTransactionStatusUseCase.kt | 8 +++++- .../OnrampTransactionRepository.kt | 7 +++++- .../factory/express/OnrampStatusFactory.kt | 1 + .../wallet/domain/OnrampStatusFactory.kt | 1 + .../SingleWalletOnrampTransactionConverter.kt | 25 +++++++------------ 6 files changed, 37 insertions(+), 26 deletions(-) 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 72a36a00db..6588bd6051 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 @@ -63,14 +63,19 @@ internal class DefaultOnrampTransactionRepository( }.map(transactionConverter::convert) } - override suspend fun updateTransactionStatus(txId: String, externalTxUrl: String, status: OnrampStatus.Status) = - withContext(dispatchers.io) { - val updatedTx = getTransactionById(txId)?.copy( - externalTxUrl = externalTxUrl, - status = status, - ) ?: return@withContext - storeTransaction(updatedTx) - } + override suspend fun updateTransactionStatus( + txId: String, + externalTxId: String, + externalTxUrl: String, + status: OnrampStatus.Status, + ) = withContext(dispatchers.io) { + val updatedTx = getTransactionById(txId)?.copy( + externalTxUrl = externalTxUrl, + externalTxId = externalTxId, + status = status, + ) ?: return@withContext + storeTransaction(updatedTx) + } override suspend fun removeTransaction(txId: String) { withContext(dispatchers.io) { diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt index 0c59d82df8..2b5e6836cc 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt @@ -10,9 +10,15 @@ class OnrampUpdateTransactionStatusUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String, externalTxUrl: String, status: OnrampStatus.Status) = Either.catch { + suspend operator fun invoke( + txId: String, + externalTxId: String, + externalTxUrl: String, + status: OnrampStatus.Status, + ) = Either.catch { onrampTransactionRepository.updateTransactionStatus( txId = txId, + externalTxId = externalTxId, externalTxUrl = externalTxUrl, status = status, ) 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 f426a5ce30..a55ad0d3b8 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 @@ -14,7 +14,12 @@ interface OnrampTransactionRepository { fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> - suspend fun updateTransactionStatus(txId: String, externalTxUrl: String, status: OnrampStatus.Status) + suspend fun updateTransactionStatus( + txId: String, + externalTxId: String, + externalTxUrl: String, + status: OnrampStatus.Status, + ) suspend fun removeTransaction(txId: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 76ca1a1052..2da3089940 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -125,6 +125,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( onrampUpdateTransactionStatusUseCase( txId = txId, externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), status = statusModel.status, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index b0347bbc46..5eb8b03010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -76,6 +76,7 @@ internal class OnrampStatusFactory @Inject constructor( onrampUpdateTransactionStatusUseCase( txId = txId, externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), status = statusModel.status, ) } 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 ce57d2ac3a..6c056b3973 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 @@ -157,7 +157,7 @@ internal class SingleWalletOnrampTransactionConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status, externalTxUrl), + link = getStatusLink(externalTxUrl), statuses = statuses, ) } @@ -264,22 +264,15 @@ internal class SingleWalletOnrampTransactionConverter( state = getStatusState(OnrampStatus.Status.Sending), ) - private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + private fun getStatusLink(externalTxUrl: String?): ExpressLinkUM { if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - clickIntents.onGoToProviderClick(externalTxUrl) - }, - ) - } - else -> ExpressLinkUM.Empty - } + return ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + clickIntents.onGoToProviderClick(externalTxUrl) + }, + ) } private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { From 240dc3f3b84f727255db6ea7a235ebfc944e36ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 16:07:29 +0500 Subject: [PATCH 14/29] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 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 250b05849d..2b9d3a318f 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 @@ -46,10 +46,8 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider import com.tangem.utils.coroutines.* -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -107,6 +105,7 @@ internal class WalletModel @Inject constructor( subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() + subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() clickIntents.initialize(innerWalletRouter, modelScope) @@ -225,7 +224,6 @@ internal class WalletModel @Inject constructor( addReferralDeepLink(selectedWallet) walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) subscribeOnExpressTransactionsUpdates(selectedWallet) - subscribeToScreenBackgroundState(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -248,39 +246,38 @@ internal class WalletModel @Inject constructor( // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground - private fun subscribeToScreenBackgroundState(userWallet: UserWallet) { + private fun subscribeToScreenBackgroundState() { screenLifecycleProvider.isBackgroundState .onEach { isBackground -> - expressTxStatusTaskScheduler.cancelTask() refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() needToRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() - subscribeOnExpressTransactionsUpdates(userWallet) } - !isBackground -> subscribeOnExpressTransactionsUpdates(userWallet) } } .launchIn(modelScope) } private fun subscribeOnExpressTransactionsUpdates(userWallet: UserWallet) { - expressTxStatusTaskScheduler.cancelTask() - expressTxStatusTaskScheduler.scheduleTask( - modelScope, - PeriodicTask( - isDelayFirst = false, - delay = EXPRESS_STATUS_UPDATE_DELAY, - task = { - runCatching { - onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) - } - }, - onSuccess = { /* no-op */ }, - onError = { /* no-op */ }, - ), - ) + if (!userWallet.isMultiCurrency) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + modelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) + } + }, + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) + } } private fun needToRefreshTimer() { @@ -295,9 +292,18 @@ internal class WalletModel @Inject constructor( val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return modelScope.launch { - refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { - Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") - } + awaitAll( + async { + refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { + Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + } + }, + async { + getWalletsUseCase.invokeSync() + .firstOrNull { it.walletId == wallet.walletCardState.id } + ?.let(::subscribeOnExpressTransactionsUpdates) + }, + ) }.saveIn(refreshWalletJobHolder) } From 77bc87a16e77830d45be565cb6417dfcd9ddbf0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 15:28:34 +0300 Subject: [PATCH 15/29] Updated on 2026-08-14 --- .../wallet/model/intents/WalletCurrencyActionsClickIntents.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index be96aa27a4..dfb386e774 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -419,6 +419,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ), ), ) + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) } } From 2d5e434c5f29fb947fc7345269aa3e41cb200ada Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 20:54:25 +0500 Subject: [PATCH 16/29] Updated on 2026-08-14 --- .../entity/OnrampSuccessComponentUM.kt | 2 + .../SetOnrampSuccessContentConverter.kt | 1 + .../OnrampSuccessComponentUMPreviewData.kt | 2 + .../model/OnrampSuccessComponentModel.kt | 140 +++++++++++------- 4 files changed, 91 insertions(+), 54 deletions(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt index cb5a0cd776..be7c79fafc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.success.entity import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.onramp.model.OnrampStatus sealed class OnrampSuccessComponentUM { @@ -18,5 +19,6 @@ sealed class OnrampSuccessComponentUM { val providerName: TextReference, val providerImageUrl: String, val notification: NotificationUM?, + val activeStatus: OnrampStatus.Status, ) : OnrampSuccessComponentUM() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index 8cc5e95401..a88f509d15 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -51,6 +51,7 @@ internal class SetOnrampSuccessContentConverter( status = value.status, externalTxId = value.externalTxId, ), + activeStatus = value.status, notification = getNotification(status = value.status, externalTxUrl = value.externalTxUrl), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt index d7e355ba63..a984b3db97 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt @@ -6,6 +6,7 @@ import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM import kotlinx.collections.immutable.persistentListOf @@ -24,6 +25,7 @@ internal data object OnrampSuccessComponentUMPreviewData { toAmount = stringReference("99.99 $"), currencyImageUrl = "", notification = null, + activeStatus = OnrampStatus.Status.Verifying, statusBlock = ExpressStatusUM( title = resourceReference(R.string.express_exchange_status_title), link = ExpressLinkUM.Empty, 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 436bcd0791..ddc3cdb858 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 @@ -21,6 +21,7 @@ import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.impl.R @@ -29,6 +30,8 @@ import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.PeriodicTask +import com.tangem.utils.coroutines.SingleTaskScheduler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates @Suppress("LongParameterList") internal class OnrampSuccessComponentModel @Inject constructor( @@ -60,6 +64,10 @@ internal class OnrampSuccessComponentModel @Inject constructor( val state: StateFlow get() = _state.asStateFlow() + private var userWallet: UserWallet by Delegates.notNull() + private var cryptoCurrency: CryptoCurrency by Delegates.notNull() + private var expressTxStatusTaskScheduler = SingleTaskScheduler() + init { loadData() } @@ -82,67 +90,87 @@ internal class OnrampSuccessComponentModel @Inject constructor( showErrorAlert(error) }, ifRight = { transaction -> - loadTransactionStatus(transaction) + userWallet = getUserWalletUseCase(transaction.userWalletId).getOrElse { + Timber.e("UserWallet found") + // this case should never happened + showErrorAlert(OnrampError.DomainError("UserWallet not found")) + return@launch + } + cryptoCurrency = getCryptoCurrencyUseCase( + userWallet = userWallet, + cryptoCurrencyId = transaction.toCurrencyId, + ).getOrElse { + Timber.e("Crypto currency not found") + showErrorAlert(OnrampError.DomainError(null)) + return@launch + } + + startStatusUpdateTask(transaction) }, ) } } - private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { - val userWallet = getUserWalletUseCase(transaction.userWalletId).getOrNull() - if (userWallet == null) { - Timber.e("UserWallet found") - // this case should never happened - showErrorAlert(OnrampError.DomainError("UserWallet not found")) - return - } - val cryptoCurrency = getCryptoCurrencyUseCase( - userWallet = userWallet, - cryptoCurrencyId = transaction.toCurrencyId, - ).getOrElse { - Timber.e("Crypto currency not found") - showErrorAlert(OnrampError.DomainError(null)) - return - } - - getOnrampStatusUseCase(userWallet = userWallet, txId = transaction.txId) - .fold( - ifLeft = { error -> - analyticsEventHandler.sendOnrampErrorEvent( - error = error, - tokenSymbol = cryptoCurrency.symbol, - providerName = transaction.providerName, - paymentMethod = transaction.paymentMethod, - ) - Timber.e(error.toString()) - showErrorAlert(error) - }, - ifRight = { status -> - analyticsEventHandler.send( - OnrampAnalyticsEvent.SuccessScreenOpened( - providerName = transaction.providerName, - currency = transaction.fromCurrency.code, - tokenSymbol = cryptoCurrency.symbol, - residence = transaction.residency, - paymentMethod = transaction.paymentMethod, - ), - ) - _state.update { - SetOnrampSuccessContentConverter( - cryptoCurrency = cryptoCurrency, - transaction = transaction, - goToProviderClick = ::goToProviderClick, - onCopyClick = ::onCopyClick, - ).convert(status) + private fun startStatusUpdateTask(transaction: OnrampTransaction) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + modelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + loadTransactionStatus(transaction) } - removeTransactionIfTerminalStatus( - cryptoCurrency = cryptoCurrency, - providerName = transaction.providerName, - paymentMethod = transaction.paymentMethod, - status = status, - ) }, - ) + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) + } + + private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { + val isTerminal = (state.value as? OnrampSuccessComponentUM.Content)?.activeStatus?.isTerminal + if (isTerminal == null || isTerminal == false) { + getOnrampStatusUseCase(userWallet = userWallet, txId = transaction.txId) + .fold( + ifLeft = { error -> + analyticsEventHandler.sendOnrampErrorEvent( + error = error, + tokenSymbol = cryptoCurrency.symbol, + providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, + ) + Timber.e(error.toString()) + showErrorAlert(error) + }, + ifRight = { status -> + analyticsEventHandler.send( + OnrampAnalyticsEvent.SuccessScreenOpened( + providerName = transaction.providerName, + currency = transaction.fromCurrency.code, + tokenSymbol = cryptoCurrency.symbol, + residence = transaction.residency, + paymentMethod = transaction.paymentMethod, + ), + ) + _state.update { + SetOnrampSuccessContentConverter( + cryptoCurrency = cryptoCurrency, + transaction = transaction, + goToProviderClick = ::goToProviderClick, + onCopyClick = ::onCopyClick, + ).convert(status) + } + removeTransactionIfTerminalStatus( + cryptoCurrency = cryptoCurrency, + providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, + status = status, + ) + }, + ) + } } private fun showErrorAlert(error: OnrampError) { @@ -178,4 +206,8 @@ internal class OnrampSuccessComponentModel @Inject constructor( } } } + + private companion object { + const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L + } } \ No newline at end of file From e9801d4e470594d3e23988a8063cc324c36978af Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 May 2025 22:35:52 +0300 Subject: [PATCH 17/29] Updated on 2026-08-14 --- .../AddStakingNotificationsTransformer.kt | 21 +++++-------------- .../StakingInfoNotificationsFactory.kt | 17 +++++++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) 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 4774ef7850..c504cad596 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 @@ -11,7 +11,6 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionN import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.StakingError @@ -23,7 +22,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -117,6 +115,7 @@ internal class AddStakingNotificationsTransformer( sendingAmount = sendingAmount, actionAmount = amountValue, feeValue = feeValue, + tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD, ) }.toImmutableList() @@ -178,9 +177,7 @@ internal class AddStakingNotificationsTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, onClick = prevState.clickIntents::openTokenDetails, ) - addTonUnstakeNotification( - actionType = prevState.actionType, - ) + addTonExtraFeeErrorNotification() addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -276,20 +273,12 @@ internal class AddStakingNotificationsTransformer( } } - private fun MutableList.addTonUnstakeNotification(actionType: StakingActionCommonType) { + private fun MutableList.addTonExtraFeeErrorNotification() { val amount = cryptoCurrencyStatusProvider().value.amount.orZero() val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.id.value - if (isTon(cryptoCurrencyNetworkIdValue) && actionType !is StakingActionCommonType.Enter) { - val notification = if (amount < TON_BALANCE_EXTRA_FEE_THRESHOLD) { - NotificationUM.Error.TonStakingExtraFeeError - } else { - StakingNotification.Info.Ordinary( - title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), - text = resourceReference(R.string.staking_notification_ton_extra_reserve_info), - ) - } - add(notification) + if (isTon(cryptoCurrencyNetworkIdValue) && amount < TON_BALANCE_EXTRA_FEE_THRESHOLD) { + add(NotificationUM.Error.TonStakingExtraFeeError) } } 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 2fe83cd25c..067bb899a0 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 @@ -37,14 +37,17 @@ internal class StakingInfoNotificationsFactory( * @param actionAmount any amount being transferred or used action * @param feeValue fee amount payed from user account */ + @Suppress("LongParameterList") fun addInfoNotifications( notifications: MutableList, prevState: StakingUiState, sendingAmount: BigDecimal, actionAmount: BigDecimal, feeValue: BigDecimal, + tonBalanceExtraFeeThreshold: BigDecimal, ) = with(notifications) { addStakingLowBalanceNotification(prevState, actionAmount) + addTonExtraFeeInfoNotification(tonBalanceExtraFeeThreshold) when (prevState.actionType) { is StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) @@ -245,6 +248,20 @@ internal class StakingInfoNotificationsFactory( } } + private fun MutableList.addTonExtraFeeInfoNotification(tonBalanceExtraFeeThreshold: BigDecimal) { + val amount = cryptoCurrencyStatusProvider().value.amount.orZero() + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.id.value + + if (isTon(cryptoCurrencyNetworkIdValue) && amount >= tonBalanceExtraFeeThreshold) { + add( + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), + text = resourceReference(R.string.staking_notification_ton_extra_reserve_info), + ), + ) + } + } + private companion object { val MINIMUM_STAKE_BALANCE = "5".toBigDecimal() val MINIMUM_RESTAKE_BALANCE = "3".toBigDecimal() From 669fd6dd321518a97ac62a4981610472e6e0a358 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 May 2025 15:00:49 +0300 Subject: [PATCH 18/29] Updated on 2026-08-14 --- .../com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt | 2 +- .../tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt | 2 +- .../tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt | 2 +- .../walletconnect/network/ethereum/WcEthMessageSignUseCase.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index dd4e607371..1fa9dc2b67 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -363,7 +363,7 @@ class VisaCardActivationTask @AssistedInject constructor( signature = response.signature, hash = dataToSign.hashToSign.hexToBytes(), publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() val signedActivationData = dataToSign.sign( rootOTP = otp.rootOTP.toHexString(), 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 55dd835a3e..5b892c93ed 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 @@ -181,7 +181,7 @@ class VisaCustomerWalletApproveTask( hash = hashToSign, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() scanCard( session = session, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 2ac037d2ec..190e4dc604 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -393,7 +393,7 @@ class WalletConnectSdkHelper { signature = signedHash, hash = hashToSign, publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() // use lowercase because some dapps cant handle UPPERCASE } } is CompletionResult.Failure -> { 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 817e5a2ddc..b173ce020d 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 @@ -86,7 +86,7 @@ object LegacySdkHelper { signature = signedHash, hash = hashToSign, publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() // use lowercase because some dapps cant handle UPPERCASE fun createMessageData(message: String): ByteArray { val messageData = try { From 78ca5446a88139910390806a4cce026ea53fc8da Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 May 2025 15:26:06 +0300 Subject: [PATCH 19/29] 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 39dae33c01..9eaa7e2fd3 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 @@ -37,7 +37,7 @@ }, { "name": "NEW_ARTWORK_LOADING", - "version": "5.24.0" + "version": "5.25.0" }, { "name": "NEW_ATTESTATION_ENABLED", From f8d3893b635a9efd04894217797af3ff1d5b041c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 May 2025 15:26:56 +0300 Subject: [PATCH 20/29] Updated on 2026-08-14 --- .../InvalidatePendingTransactionsUseCase.kt | 18 ++++++++++++++++-- .../impl/presentation/model/StakingModel.kt | 6 +++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 40a8c174a9..2b1a377476 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.utils.extensions.isEqualTo @@ -15,13 +16,13 @@ class InvalidatePendingTransactionsUseCase( operator fun invoke( balanceItems: List, - processingActions: List, + stakingActions: List, token: Token, ): Either> { return Either.catch { val balancesToDisplay = mergeBalancesAndProcessingActions( realBalances = balanceItems, - processingActions = processingActions, + processingActions = stakingActions.filter { it.status == StakingActionStatus.PROCESSING }, token = token, ) balancesToDisplay @@ -68,6 +69,8 @@ class InvalidatePendingTransactionsUseCase( // intentionally do nothing } } + + doPostProcessing(balances, action, token) } return balances @@ -149,4 +152,15 @@ class InvalidatePendingTransactionsUseCase( } return index to action.amount } + + private fun doPostProcessing(balances: MutableList, action: StakingAction, token: Token) { + val validatorAddress = action.validatorAddress ?: action.validatorAddresses?.firstOrNull() + if (token.network == NetworkType.TON && validatorAddress != null) { + for (index in balances.indices) { + if (balances[index].validatorAddress == validatorAddress) { + balances[index] = balances[index].copy(isPending = true) + } + } + } + } } \ No newline at end of file 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 d122cd48bf..3edb0cb035 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 @@ -145,7 +145,7 @@ internal class StakingModel @Inject constructor( } private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var processingActions: List = emptyList() + private var stakingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var minimumTransactionAmount: EnterAmountBoundary? = null @@ -157,7 +157,7 @@ internal class StakingModel @Inject constructor( val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data return invalidatePendingTransactionsUseCase( balanceItems = yieldBalance?.balance?.items ?: emptyList(), - processingActions = processingActions, + stakingActions = stakingActions, token = yield.token, ).getOrElse { emptyList() } } @@ -1028,7 +1028,7 @@ internal class StakingModel @Inject constructor( .distinctUntilChanged() .onEach { result -> result.getOrNull()?.let { actions -> - processingActions = actions + stakingActions = actions if (isInitState()) { updateInitialData(status) } From df3d24237f72599b8837ac644d5d5600f83587ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 May 2025 15:27:11 +0300 Subject: [PATCH 21/29] Updated on 2026-08-14 --- .../domain/markets/GetStakingNotificationMaxApyUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a14d6c7cbd..4ccaea6680 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt @@ -24,7 +24,7 @@ class GetStakingNotificationMaxApyUseCase( flow2 = marketsTokenRepository.getMaxApy(), ) { hideClicked, maxApy -> val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) { - currentDate - walletFirstUsageDate < TWO_WEEKS_IN_MILLIS + currentDate - walletFirstUsageDate > TWO_WEEKS_IN_MILLIS } else { false } From 92f2c8fcf03ac736fb8232f07e8a0397819466a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 May 2025 15:27:35 +0300 Subject: [PATCH 22/29] Updated on 2026-08-14 --- .../presentation/state/converters/YieldBalancesConverter.kt | 5 ++--- .../state/factory/TokenDetailsStakingInfoConverter.kt | 5 ++--- .../src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt | 4 ++++ 3 files changed, 8 insertions(+), 6 deletions(-) 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 e663307922..14455e2398 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 @@ -11,8 +11,7 @@ import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.YieldReward -import com.tangem.lib.crypto.BlockchainUtils.isBSC -import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -87,7 +86,7 @@ internal class YieldBalancesConverter( val isRewardsClaimable = rewards?.isNotEmpty() == true return when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isStakingRewardUnavailable(blockchainId) -> RewardBlockType.RewardUnavailable isRewardsClaimable && isActionable -> RewardBlockType.Rewards isRewardsClaimable && !isActionable -> RewardBlockType.RewardsRequirementsError else -> RewardBlockType.NoRewards 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 9b5b230751..af44d1ff50 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 @@ -20,8 +20,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.features.tokendetails.impl.R -import com.tangem.lib.crypto.BlockchainUtils.isBSC -import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero @@ -146,7 +145,7 @@ internal class TokenDetailsStakingInfoConverter( private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { val blockchainId = status.currency.network.id.value val rewardBlockType = when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isStakingRewardUnavailable(blockchainId) -> RewardBlockType.RewardUnavailable stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards else -> RewardBlockType.Rewards } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 71205a6291..993226dafd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -148,6 +148,10 @@ object BlockchainUtils { return blockchain != Blockchain.Cardano } + fun isStakingRewardUnavailable(blockchainId: String): Boolean { + return isSolana(blockchainId) || isBSC(blockchainId) || isTon(blockchainId) + } + private fun getNetworkStandardName(blockchain: Blockchain): String { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20" From bca3dd34599598c8021988f60bf39768319ae0a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 May 2025 10:43:14 +0300 Subject: [PATCH 23/29] Updated on 2026-08-14 --- .idea/codeStyles/Project.xml | 33 ++++++++++ .../tap/features/home/DefaultHomeComponent.kt | 16 +---- .../com/tangem/core/ui/res/TangemTheme.kt | 2 +- .../tangem/core/ui/utils/TangemThemeUtils.kt | 31 +++++++++ .../com/tangem/domain/wallets/models/Test.kt | 66 +++++++++++++++++++ .../stories/impl/DefaultStoriesComponent.kt | 15 +---- 6 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/TangemThemeUtils.kt create mode 100644 domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/Test.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 8f2af51f2d..9d98629279 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -5,6 +5,39 @@