diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index a21fee2f05..143a8d92a4 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -29,6 +29,7 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.swap.SwapFeatureToggles import com.tangem.tap.common.extensions.setContext @@ -300,19 +301,32 @@ internal class MainViewModel @Inject constructor( private fun preloadImages() { try { viewModelScope.launch { - val swapStories = if (swapFeatureToggles.isPromoStoriesEnabled) { - getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id) - .getOrNull() - } else { - null - } + if (swapFeatureToggles.isPromoStoriesEnabled) { + val swapStories = getStoryContentUseCase.invokeSync( + id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, + refresh = true, + ).getOrNull() - val storiesImages = swapStories?.getImageUrls() - // try to preload images for stories - storiesImages?.forEach(imagePreloader::preload) + if (swapStories == null) { + analyticsEventHandler.send( + StoriesEvents.Error( + type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, + ), + ) + } else { + val storiesImages = swapStories.getImageUrls() + // try to preload images for stories + storiesImages.forEach(imagePreloader::preload) + } + } } } catch (ex: Exception) { Timber.e(ex.message) + analyticsEventHandler.send( + StoriesEvents.Error( + type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, + ), + ) } } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt index f5d2e66df6..baf5001db7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -43,7 +43,7 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { id = R.string.warning_receive_blocked_hedera_token_association_required_message, ) ScenarioUnavailabilityReason.UsedOutdatedData -> { - resourceReference(id = R.string.token_button_unavailability_reason_out_od_date_balance) + resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance) } ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index e3ee5ae7a0..24fc6cd8b2 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -20,7 +21,6 @@ import com.tangem.utils.converter.Converter * @property onClick lambda be invoked when item is clicked * @property appCurrency selected app currency * @property balance wallet balance - * @property isLoading wallet loading state * @property isBalanceHidden wallet balance is hidden * [REDACTED_AUTHOR] @@ -29,7 +29,6 @@ class UserWalletItemUMConverter( private val onClick: (UserWalletId) -> Unit, private val appCurrency: AppCurrency? = null, private val balance: TotalFiatBalance? = null, - private val isLoading: Boolean = true, private val isBalanceHidden: Boolean = false, private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, ) : Converter { @@ -75,7 +74,7 @@ class UserWalletItemUMConverter( UserWalletItemUM.Balance.Loaded( value = formattedAmount, - isFlickering = isLoading, + isFlickering = balance.source == StatusSource.CACHE, ) } else { UserWalletItemUM.Balance.Failed 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 92d4653d08..d2e46b6fea 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 @@ -29,7 +29,7 @@ }, { "name": "SWAP_STORIES_ENABLED", - "version": "undefined" + "version": "5.21.0" }, { "name": "VISA_ONBOARDING_ENABLED", diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index ecef79cb21..e4e17b8896 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -271,6 +271,8 @@ Getauscht von %s Besuche die Website des Anbieters, um dein Geld zurückzuerhalten Fehler beim Vorgang durch Anbieter + Dein Wechsel dauert länger als erwartet. Wende Dich für Hilfe bitte an den Support Deines Anbieters. + Lange Transaktionszeit Der Transaktionsbetrag wurde aufgrund von OKX- oder Bridge-Regeln in %1$s auf deine Wallet zurückerstattet. %2$s Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet. Besuche die Website des Anbieters zur Überprüfung @@ -813,6 +815,10 @@ Niedriges Stakingguthaben Für erneutes Staking sind mindestens %1$s %2$s erforderlich. Bitte lade Dein Guthaben auf. Nicht genug %s + Unzureichendes Guthaben zum Staking + Für das erneute staken sind mindestens 3 ADA erforderlich. Bitte lade Dein Guthaben auf. + Nicht genug ADA + Der für das Staking erforderliche Mindestbetrag muss 5 ADA überschreiten. Bitte lade Dein Guthaben auf, um mit dem Staking zu beginnen. Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut. Wenn Du im %1$s -Netzwerk mit einem neuen Validator stakest, werden alle zuvor eingesetzten Gelder automatisch an diesen Validator übertragen. Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. @@ -880,6 +886,8 @@ Lerne Tangem kennen Tausche, kaufen Sie NFTs, vergebe Kredite und tätige Einlagen bei mehr als 100 verschiedenen dezentralen Diensten Web 3.0-kompatibel + Zum Fortfahren ist eine eingehende Transaktion von mindestens %1$s erforderlich + Unzureichende Mittel Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! @@ -918,6 +926,7 @@ Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen. Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können. Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. + Der angezeigte Kontostand kann aufgrund der Zwischenspeicherung veraltet sein. Der Verkauf von Geldmitteln ist möglich, sobald die ausstehende(n) Transaktion(en) im %s -Netzwerk abgeschlossen ist/sind. Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. Der Verkauf von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. @@ -978,6 +987,9 @@ Wallet umbenennen Alle freischalten Alle mit %s freischalten + Freischalten + Scanne Deine Karte, um den Zugriff freizuschalten + Benötigte Freischaltung Blockchain ist nicht erreichbar. Versuche es später nochmal Karte oder Ring scannen Dieses Wallet wurde bereits früher aktiviert. \n Wenn dies nicht von Dir durchgeführt wurde, wende Dich bitte an den Support. \n Tangem verkauft niemals Wallets zusammen mit vorgenerierten Zugangscodes. @@ -1092,7 +1104,7 @@ Du musst deinen Token zuordnen, bevor du Token erhalten kannst Netzmietgebühr erforderlich Handlungsbedarf - Hast Du innerhalb von 7 Tagen nach dem Erstellen einer Wallet per App oder E-Mail Kontakt zum Support aufgenommen? Wenn ja oder wenn Du Dir nicht sicher bist, befolge die Anweisungen und führe diese vollständig aus. + Hast Du innerhalb von 7 Tagen nach der Erstellung Ihres Wallets über die App Kontakt zum Support aufgenommen? Wenn ja oder wenn Du Dir nicht sicher bist, klicke auf „Ja“ und folge den Anweisungen. Vielen Dank! Alles erledigt! Keine weiteren Maßnahmen erforderlich. Sie werden jetzt auf die offizielle Tangem-Website weitergeleitet. Bitte lesen und befolgen Sie dort die Anweisungen. Hast Du das Tangem-Supportteam schon einmal direkt über diese Anwendung kontaktiert? @@ -1107,6 +1119,7 @@ Einige Token-Guthaben konnten nicht aktualisiert werden Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. Nur für Testzwecke + Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite. Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9535f46439..a89e339efe 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -805,6 +805,10 @@ ステーキング残高が低いです 再ステーキングには最低%1$s %2$sが必要です。残高を補充してください。 %sが足りません + ステーキングのための残高が不足しています + リステーキングには最低3ADA が必要です。残高を補充してください。 + ADAが不足しています + ステーキングに必要な最小金額は5ADAを超える必要があります。ステーキングを開始するには残高を補充してください。 ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。 新しいバリデーターを使用して%1$sネットワークにステーキングすると、以前にステーキングされた資金がすべてこのバリデーターに自動的に転送されます。 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 @@ -872,6 +876,8 @@ Tangemのご紹介 100種類以上の分散型サービスで、NFTの交換・購入や、借入・預金を行うことができます。 Web3.0対応 + 続行するには少なくとも%1$sの受信取引が必要です + 残高不足 ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! @@ -910,6 +916,7 @@ 売却できる資金がありません。アカウントに入金して、売却できるようにしてください。 送金する資金がありません。アカウントに入金して、送金できるようにしてください。 %sのスワップは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 + 表示残高はキャッシュにより古くなっている可能性があります。 %sネットワーク上の保留中の取引が完了すると、資金の売却が可能になります。 ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 %sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 @@ -970,6 +977,9 @@ ウォレット名の変更 すべてのロックを解除 %sですべてをロック解除 + ロック解除 + カードをスキャンしてアクセスロックを解除する + ロック解除が必要 ブロックチェーンにアクセスできません。後でもう一度お試しください。 カードまたはリングをスキャン このウォレットはすでに有効化されています。\nあなたが行ったのでない場合は、サポートに連絡してください。\nTangemは、事前に生成されたアクセスコードと一緒にウォレットを販売することはありません。 @@ -1083,7 +1093,7 @@ トークンを受け取る前に、トークンを関連付ける必要があります。 ネットワーク使用料が必要です 対応が必要です - ウォレットを作成してから7日以内にアプリまたはEメール経由でサポートに連絡しましたか?連絡したか不明な場合は、手順に従って完了してください。 + ウォレットの作成後、7日以内にアプリ経由でサポートに連絡しましたか?連絡した場合、あるいは連絡したかわからない場合は、「はい」をクリックして指示に従ってください。 ありがとうございます。準備完了です。これ以上の操作は必要ありません。 Tangemの公式ウェブサイトに移動します。そちらの指示に従ってください。 このアプリ経由でTangemサポートチームに直接連絡したことがありますか? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 016834185c..eaaa71beea 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1111,6 +1111,7 @@ Подобається Tangem? Вам необхідно провести асоціацію токена, щоб мати можливість приймати його Необхідна плата за оренду мережі + Чи зверталися в службу підтримки через додаток протягом 7 днів після створення гаманця? Якщо зверталися або не впевнені, натисніть «Так» і дотримуйтесь інструкцій. Дякуємо! Все добре! Подальших дій не потрібно. Ви будете перенаправлені на офіційний сайт Tangem. Будь ласка, прочитайте та виконайте вказані там інструкції. Ви коли-небудь зверталися до служби підтримки Tangem безпосередньо з цього додатку? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8efdb73f1b..258318407d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -926,7 +926,7 @@ You do not have funds to sell. Top up your account to be able to sell funds from it. You do not have funds to send. Top up your account to be able to send funds from it. Swapping %s is not supported by current providers, but we are working to add more options. - The displayed balance might be outdated due to caching. + The displayed balance might be outdated due to caching. Selling funds will be available once the pending transaction(s) on the %s network is complete. Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not supported by current providers, but we are working to add more options. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 63807a2a7d..77f984d3c8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -30,9 +28,9 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.components.icons.badge.drawBadge import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -225,20 +223,6 @@ fun getTextColor(config: ActionButtonConfig): Color { } } -private fun DrawScope.drawBadge(containerColor: Color) { - val width = size.width - drawCircle( - color = containerColor, - center = Offset(x = width - 2.dp.toPx(), y = 2.dp.toPx()), - radius = 5.dp.toPx(), - ) - drawCircle( - color = TangemColorPalette.Azure, - center = Offset(x = width - 2.dp.toPx(), y = 2.dp.toPx()), - radius = 3.dp.toPx(), - ) -} - @Preview(group = "RoundedActionButton", showBackground = true) @Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/badge/DotBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/badge/DotBadge.kt new file mode 100644 index 0000000000..e25b5c333a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/badge/DotBadge.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.components.icons.badge + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemColorPalette + +fun DrawScope.drawBadge(containerColor: Color, offset: Dp = 2.dp) { + val width = size.width + drawCircle( + color = containerColor, + center = Offset(x = width - offset.toPx(), y = offset.toPx()), + radius = 5.dp.toPx(), + ) + drawCircle( + color = TangemColorPalette.Azure, + center = Offset(x = width - offset.toPx(), y = offset.toPx()), + radius = 3.dp.toPx(), + ) +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 1844c1e6c5..86807b4905 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -144,10 +144,10 @@ internal class DefaultHotCryptoRepository( return copy( tokens = this.tokens + .applyTokensIdMigrations() .filter { hotToken -> !tokens.hasAlreadyAdded(hotToken) && userWallet.canHandleHotCrypto(hotToken) - } - .applyTokensIdMigrations(), + }, ) } diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 1e0f94e8f0..5b14b75e9a 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -44,15 +44,15 @@ internal class DefaultPromoRepository( } override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { - getStoryByIdSync(id) + getStoryByIdSync(id = id, refresh = false) } - override suspend fun getStoryByIdSync(id: String): StoryContent? = withContext(dispatchers.io) { + override suspend fun getStoryByIdSync(id: String, refresh: Boolean): StoryContent? = withContext(dispatchers.io) { if (!isReadyToShowStoriesSync(id)) return@withContext null val storedPromo = promoStoriesStore.getSyncOrNull(storyId = id) // Get last stored promo by id if possible or get from network - val story = if (storedPromo == null) { + val story = if (storedPromo == null && refresh) { val storyContent = runCatching { // Important to return withTimeoutOrNull(STORIES_LOAD_DELAY) { diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt index 1964bb9b6e..786f1e9552 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt @@ -18,7 +18,7 @@ class GetStoryContentUseCase( .catch { emit(it.left()) } .onEmpty { emit(null.right()) } - suspend fun invokeSync(id: String): Either = Either.catch { - promoRepository.getStoryByIdSync(id) + suspend fun invokeSync(id: String, refresh: Boolean = false): Either = Either.catch { + promoRepository.getStoryByIdSync(id, refresh) } } \ 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 d785801863..4db7b9bf87 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 @@ -18,7 +18,7 @@ interface PromoRepository { // region Stories fun getStoryById(id: String): Flow - suspend fun getStoryByIdSync(id: String): StoryContent? + suspend fun getStoryByIdSync(id: String, refresh: Boolean): StoryContent? fun isReadyToShowStories(storyId: String): Flow diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 767dfd45ab..dcea766313 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -90,7 +90,7 @@ sealed class TokenScreenAnalyticsEvent( ) class NoticeActionInactive(token: String, tokenAction: TokenAction, reason: String) : TokenScreenAnalyticsEvent( - "Notice - Action Inactive", + event = "Notice - Action Inactive", params = buildMap { put("Token", token) put("Action", tokenAction.action) @@ -112,6 +112,7 @@ sealed class TokenScreenAnalyticsEvent( private const val UNAVAILABLE = "Unavailable" private const val EMPTY = "Empty" private const val PENDING = "Pending" + private const val CACHING = "Caching" fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String { return when (this) { @@ -123,9 +124,9 @@ sealed class TokenScreenAnalyticsEvent( -> UNAVAILABLE is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY is ScenarioUnavailabilityReason.PendingTransaction -> PENDING + ScenarioUnavailabilityReason.UsedOutdatedData -> CACHING ScenarioUnavailabilityReason.None, ScenarioUnavailabilityReason.Unreachable, - ScenarioUnavailabilityReason.UsedOutdatedData, -> "" } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index eafad782ed..408c4e81ca 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -49,6 +49,14 @@ class FetchTokenListUseCase( ): Either = either { val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies) + invoke(userWalletId = userWalletId, currencies = currencies, mode = mode) + } + + suspend operator fun invoke( + userWalletId: UserWalletId, + currencies: List, + mode: RefreshMode = RefreshMode.NONE, + ): Either = either { coroutineScope { val fetchStatuses = async { fetchNetworksStatuses( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 5f874b7419..8b6ba7ade2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -13,10 +13,8 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest import timber.log.Timber class GetWalletTotalBalanceUseCase( @@ -54,20 +52,28 @@ class GetWalletTotalBalanceUseCase( } } - @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(userWalletId: UserWalletId): LceFlow { - return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId) - .transformLatest { maybeStatuses -> - val balance = createBalance(maybeStatuses) - - emit(balance) - } + return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId).map(::createBalance) } private fun createBalance( maybeStatuses: Lce>, ): Lce = lce { - val statuses = maybeStatuses.bind() + val statuses = when (maybeStatuses) { + is Lce.Content -> maybeStatuses.content + is Lce.Error -> raise(maybeStatuses) + is Lce.Loading -> { + val content = maybeStatuses.partialContent + + if (content == null) { + isLoading.set(true) + + raise(lceLoading()) + } else { + content + } + } + } val operations = TokenListFiatBalanceOperations( currencies = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() }, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index cd0a3639c9..969e56d6ef 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -7,6 +7,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.core.utils.lceError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -45,9 +46,14 @@ class CachedCurrenciesStatusesOperations( ): LceFlow> = lceFlow { currenciesFlow.flatMapLatest { maybeCurrencies -> val isUpdating = MutableStateFlow(value = true) - val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() - ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } + val nonEmptyCurrencies = maybeCurrencies + .getOrElse { return@flatMapLatest flowOf(it.lceError()) } + .toNonEmptyListOrNull() + + if (nonEmptyCurrencies.isNullOrEmpty()) { + return@flatMapLatest flowOf(TokenListError.EmptyTokens.lceError()) + } // This is only 'true' when the flow here is empty, such as during initial loading if (isLoading.get()) { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index a29ea393b2..4b10237030 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -40,9 +40,9 @@ internal class UserWalletListModel @Inject constructor( init { combine( - userWalletsFetcher.userWallets, - shouldSaveUserWalletsUseCase(), - isWalletSavingInProgress, + flow = userWalletsFetcher.userWallets, + flow2 = shouldSaveUserWalletsUseCase(), + flow3 = isWalletSavingInProgress, transform = ::updateState, ).launchIn(modelScope) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index 8de287944f..eef9deef0d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -49,9 +49,9 @@ internal class UserWalletsFetcher @Inject constructor( emit(uiModels) combine( - getSelectedAppCurrencyUseCase().distinctUntilChanged(), - getBalanceHidingSettingsUseCase().distinctUntilChanged(), - getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), + flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), + flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), ) { maybeAppCurrency, balanceHidingSettings, maybeBalances -> val models = createUiModels( wallets = wallets, @@ -99,7 +99,6 @@ internal class UserWalletsFetcher @Inject constructor( appCurrency = appCurrency, balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, - isLoading = maybeBalances.isLoading(), ) .convert(userWallet) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 5f82c6c481..aecd1f943e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -127,7 +127,6 @@ internal class AddToPortfolioBSContentUMFactory( }, appCurrency = portfolioData.appCurrency, balance = balance?.getOrNull(), - isLoading = balance?.isLoading() == true, isBalanceHidden = portfolioData.isBalanceHidden, endIcon = if (userWallet.walletId == selectedWalletId) { UserWalletItemUM.EndIcon.Checkmark diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 946fcbdd07..6f1fe73641 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -48,33 +48,14 @@ internal class PortfolioTokenUMConverter( private fun quickActions(cryptoData: PortfolioData.CryptoCurrencyData): PortfolioTokenUM.QuickActions { return PortfolioTokenUM.QuickActions( - actions = listOfNotNull( - QuickActionUM.Buy.takeIf { - cryptoData.actions.any { - it is TokenActionsState.ActionState.Buy && - it.unavailabilityReason == ScenarioUnavailabilityReason.None - } - }, - QuickActionUM.Exchange.takeIf { - cryptoData.actions.any { - it is TokenActionsState.ActionState.Swap && - it.unavailabilityReason == ScenarioUnavailabilityReason.None - } - }, - QuickActionUM.Receive.takeIf { - cryptoData.actions.any { - it is TokenActionsState.ActionState.Receive && - it.unavailabilityReason == ScenarioUnavailabilityReason.None - } - }, - ).toImmutableList(), + actions = toQuickActions(cryptoData.actions), onQuickActionClick = { when (it) { QuickActionUM.Buy -> tokenActionsHandler.handle( action = TokenActionsBSContentUM.Action.Buy, cryptoCurrencyData = cryptoData, ) - QuickActionUM.Exchange -> tokenActionsHandler.handle( + is QuickActionUM.Exchange -> tokenActionsHandler.handle( action = TokenActionsBSContentUM.Action.Exchange, cryptoCurrencyData = cryptoData, ) @@ -94,4 +75,17 @@ internal class PortfolioTokenUMConverter( }, ) } + + private fun toQuickActions(actions: List) = buildList { + actions.forEach { action -> + if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { + when (action) { + is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy + is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge) + is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive + else -> null + }?.let(::add) + } + } + }.toImmutableList() } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt index ba5ff99531..e945b00c26 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -4,7 +4,10 @@ import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button @@ -13,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection @@ -22,6 +26,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.icons.badge.drawBadge import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager @@ -29,7 +34,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable internal fun PortfolioQuickActions( @@ -59,7 +64,6 @@ internal fun PortfolioQuickActions( } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { val lineColor = TangemTheme.colors.stroke.primary @@ -99,7 +103,7 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) } } -@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable private fun AnimatedVisibilityScope.QuickActionItem( state: QuickActionUM, @@ -131,27 +135,7 @@ private fun AnimatedVisibilityScope.QuickActionItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { - Box( - Modifier - .animateEnterExit( - enter = scaleIn(), - exit = scaleOut(), - ) - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ) - .size(TangemTheme.dimens.size32), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.button.primary, - ) - } + QuickActionIcon(state) Column( modifier = Modifier .animateEnterExit( @@ -174,6 +158,38 @@ private fun AnimatedVisibilityScope.QuickActionItem( } } +@Composable +private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { + val containerColor = TangemTheme.colors.background.action + Box( + Modifier + .animateEnterExit( + enter = scaleIn(), + exit = scaleOut(), + ) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size32) + .drawWithContent { + drawContent() + if (state is QuickActionUM.Exchange && state.showBadge) { + drawBadge(containerColor = containerColor, offset = 4.dp) + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(TangemTheme.dimens.size16), + imageVector = ImageVector.vectorResource(id = state.icon), + contentDescription = null, + tint = TangemTheme.colors.button.primary, + ) + } +} + @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -197,7 +213,11 @@ private fun Preview() { modifier = Modifier.background(color = TangemTheme.colors.background.action), ) { PortfolioQuickActions( - actions = QuickActionUM.entries.toImmutableList(), + actions = persistentListOf( + QuickActionUM.Buy, + QuickActionUM.Exchange(showBadge = true), + QuickActionUM.Receive, + ), isVisible = isVisible, onActionClick = {}, onActionLongClick = {}, @@ -214,7 +234,11 @@ private fun PreviewRtl() { TangemThemePreview(rtl = true) { Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { PortfolioQuickActions( - actions = QuickActionUM.entries.toImmutableList(), + actions = persistentListOf( + QuickActionUM.Buy, + QuickActionUM.Exchange(showBadge = true), + QuickActionUM.Receive, + ), isVisible = true, onActionClick = {}, onActionLongClick = {}, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 79177f24a4..d1171dc444 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -10,7 +10,6 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { @@ -61,7 +60,11 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider if (swapStory == null) { - analyticsEventHandler.send( - StoriesEvents.Error( - source = params.screenSource, - type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, - ), - ) openScreen(hideStories = false) // Fallback to target screen } else { _state.update { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt index 6123fa2876..b7f89e9288 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt @@ -21,12 +21,10 @@ sealed class StoriesEvents( ) data class Error( - val source: String, val type: String, ) : StoriesEvents( event = "Error", params = mapOf( - AnalyticsParam.SOURCE to source, AnalyticsParam.TYPE to type, ), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 05a89386ef..1728183620 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -82,7 +82,6 @@ internal class SwapModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val blockchainInteractor: BlockchainInteractor, private val analyticsEventHandler: AnalyticsEventHandler, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, @@ -96,6 +95,7 @@ internal class SwapModel @Inject constructor( private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val featureToggles: SwapFeatureToggles, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, swapInteractorFactory: SwapInteractor.Factory, private val urlOpener: UrlOpener, router: AppRouter, @@ -278,23 +278,10 @@ internal class SwapModel @Inject constructor( getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id).fold( ifLeft = { Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}") - analyticsEventHandler.send( - StoriesEvents.Error( - source = params.screenSource, - type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, - ), - ) }, ifRight = { story -> if (story != null) { uiState = stateBuilder.createStoriesState(uiState, story) - } else { - analyticsEventHandler.send( - StoriesEvents.Error( - source = params.screenSource, - type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, - ), - ) } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 0b83df64af..aab061ab81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -149,7 +149,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ).fold( ifLeft = { Timber.e(it, "Failed to derive public keys") }, ifRight = { - fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES).onLeft { + fetchTokenListUseCase( + userWalletId = userWallet.walletId, + mode = RefreshMode.SKIP_CURRENCIES, + currencies = missedAddressCurrencies, + ).onLeft { Timber.e("Unable to refresh token list: $it") } },