Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-27 12:28:56 +03:00
commit d8527a5f3f
30 changed files with 214 additions and 161 deletions

View file

@ -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,
),
)
}
}
}

View file

@ -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")

View file

@ -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<UserWallet, UserWalletItemUM> {
@ -75,7 +74,7 @@ class UserWalletItemUMConverter(
UserWalletItemUM.Balance.Loaded(
value = formattedAmount,
isFlickering = isLoading,
isFlickering = balance.source == StatusSource.CACHE,
)
} else {
UserWalletItemUM.Balance.Failed

View file

@ -29,7 +29,7 @@
},
{
"name": "SWAP_STORIES_ENABLED",
"version": "undefined"
"version": "5.21.0"
},
{
"name": "VISA_ONBOARDING_ENABLED",

View file

@ -271,6 +271,8 @@
<string name="express_exchange_by">Getauscht von %s</string>
<string name="express_exchange_notification_failed_text">Besuche die Website des Anbieters, um dein Geld zurückzuerhalten</string>
<string name="express_exchange_notification_failed_title">Fehler beim Vorgang durch Anbieter</string>
<string name="express_exchange_notification_long_transaction_time_text">Dein Wechsel dauert länger als erwartet. Wende Dich für Hilfe bitte an den Support Deines Anbieters.</string>
<string name="express_exchange_notification_long_transaction_time_title">Lange Transaktionszeit</string>
<string name="express_exchange_notification_refund_text">Der Transaktionsbetrag wurde aufgrund von OKX- oder Bridge-Regeln in %1$s auf deine Wallet zurückerstattet. %2$s</string>
<string name="express_exchange_notification_refund_title">Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet.</string>
<string name="express_exchange_notification_verification_text">Besuche die Website des Anbieters zur Überprüfung</string>
@ -813,6 +815,10 @@
<string name="staking_notification_low_staked_balance_title">Niedriges Stakingguthaben</string>
<string name="staking_notification_minimum_balance_error_text">Für erneutes Staking sind mindestens %1$s %2$s erforderlich. Bitte lade Dein Guthaben auf.</string>
<string name="staking_notification_minimum_balance_error_title">Nicht genug %s</string>
<string name="staking_notification_minimum_balance_title">Unzureichendes Guthaben zum Staking</string>
<string name="staking_notification_minimum_restake_ada_text">Für das erneute staken sind mindestens 3 ADA erforderlich. Bitte lade Dein Guthaben auf.</string>
<string name="staking_notification_minimum_restake_ada_title">Nicht genug ADA</string>
<string name="staking_notification_minimum_stake_ada_text">Der für das Staking erforderliche Mindestbetrag muss 5 ADA überschreiten. Bitte lade Dein Guthaben auf, um mit dem Staking zu beginnen.</string>
<string name="staking_notification_network_error_text">Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="staking_notification_new_validator_funds_transfer">Wenn Du im %1$s -Netzwerk mit einem neuen Validator stakest, werden alle zuvor eingesetzten Gelder automatisch an diesen Validator übertragen.</string>
<string name="staking_notification_restake_rewards_text">Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn.</string>
@ -880,6 +886,8 @@
<string name="story_meet_title">Lerne Tangem kennen</string>
<string name="story_web3_description">Tausche, kaufen Sie NFTs, vergebe Kredite und tätige Einlagen bei mehr als 100 verschiedenen dezentralen Diensten</string>
<string name="story_web3_title">Web 3.0-kompatibel</string>
<string name="sui_not_enough_coin_for_fee_description">Zum Fortfahren ist eine eingehende Transaktion von mindestens %1$s erforderlich</string>
<string name="sui_not_enough_coin_for_fee_title">Unzureichende Mittel</string>
<string name="swap_give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen.</string>
<string name="swap_promo_text">Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche.</string>
<string name="swap_promo_title">Neuer Swap-Anbieter verfügbar!</string>
@ -918,6 +926,7 @@
<string name="token_button_unavailability_reason_empty_balance_sell">Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen.</string>
<string name="token_button_unavailability_reason_empty_balance_send">Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen.</string>
<string name="token_button_unavailability_reason_out_of_date_balance">Der angezeigte Kontostand kann aufgrund der Zwischenspeicherung veraltet sein.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Der Verkauf von Geldmitteln ist möglich, sobald die ausstehende(n) Transaktion(en) im %s -Netzwerk abgeschlossen ist/sind.</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind.</string>
<string name="token_button_unavailability_reason_sell_unavailable">Der Verkauf von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen.</string>
@ -978,6 +987,9 @@
<string name="user_wallet_list_rename_popup_title">Wallet umbenennen</string>
<string name="user_wallet_list_unlock_all">Alle freischalten</string>
<string name="user_wallet_list_unlock_all_with">Alle mit %s freischalten</string>
<string name="visa_unlock_notification_button">Freischalten</string>
<string name="visa_unlock_notification_subtitle">Scanne Deine Karte, um den Zugriff freizuschalten</string>
<string name="visa_unlock_notification_title">Benötigte Freischaltung</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain ist nicht erreichbar. Versuche es später nochmal</string>
<string name="wallet_balance_missing_derivation">Karte oder Ring scannen</string>
<string name="wallet_been_activated_message">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.</string>
@ -1092,7 +1104,7 @@
<string name="warning_receive_blocked_hedera_token_association_required_message">Du musst deinen Token zuordnen, bevor du Token erhalten kannst</string>
<string name="warning_rent_fee_title">Netzmietgebühr erforderlich</string>
<string name="warning_seedphrase_action_required_title">Handlungsbedarf</string>
<string name="warning_seedphrase_contacted_support">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.</string>
<string name="warning_seedphrase_contacted_support">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.</string>
<string name="warning_seedphrase_issue_answer_no">Vielen Dank! Alles erledigt! Keine weiteren Maßnahmen erforderlich.</string>
<string name="warning_seedphrase_issue_answer_yes">Sie werden jetzt auf die offizielle Tangem-Website weitergeleitet. Bitte lesen und befolgen Sie dort die Anweisungen.</string>
<string name="warning_seedphrase_issue_message">Hast Du das Tangem-Supportteam schon einmal direkt über diese Anwendung kontaktiert?</string>
@ -1107,6 +1119,7 @@
<string name="warning_some_token_balances_not_updated">Einige Token-Guthaben konnten nicht aktualisiert werden</string>
<string name="warning_testnet_card_message">Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden.</string>
<string name="warning_testnet_card_title">Nur für Testzwecke</string>
<string name="warning_token_balance_not_updated">Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite.</string>
<string name="welcome_interrupted_backup_alert_discard">Verwerfen</string>
<string name="welcome_interrupted_backup_alert_message">Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen?</string>
<string name="welcome_interrupted_backup_alert_resume">Ja, fortsetzen</string>

View file

@ -805,6 +805,10 @@
<string name="staking_notification_low_staked_balance_title">ステーキング残高が低いです</string>
<string name="staking_notification_minimum_balance_error_text">再ステーキングには最低%1$s %2$sが必要です。残高を補充してください。</string>
<string name="staking_notification_minimum_balance_error_title">%sが足りません</string>
<string name="staking_notification_minimum_balance_title">ステーキングのための残高が不足しています</string>
<string name="staking_notification_minimum_restake_ada_text">リステーキングには最低3ADA が必要です。残高を補充してください。</string>
<string name="staking_notification_minimum_restake_ada_title">ADAが不足しています</string>
<string name="staking_notification_minimum_stake_ada_text">ステーキングに必要な最小金額は5ADAを超える必要があります。ステーキングを開始するには残高を補充してください。</string>
<string name="staking_notification_network_error_text">ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。</string>
<string name="staking_notification_new_validator_funds_transfer">新しいバリデーターを使用して%1$sネットワークにステーキングすると、以前にステーキングされた資金がすべてこのバリデーターに自動的に転送されます。</string>
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
@ -872,6 +876,8 @@
<string name="story_meet_title">Tangemのご紹介</string>
<string name="story_web3_description">100種類以上の分散型サービスで、NFTの交換・購入や、借入・預金を行うことができます。</string>
<string name="story_web3_title">Web3.0対応</string>
<string name="sui_not_enough_coin_for_fee_description">続行するには少なくとも%1$sの受信取引が必要です</string>
<string name="sui_not_enough_coin_for_fee_title">残高不足</string>
<string name="swap_give_permission_fee_footer">ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。</string>
<string name="swap_promo_text">より多くのトークンをより良いレートで、ウォレット内にて直接交換します。</string>
<string name="swap_promo_title">新しいスワッププロバイダーが利用可能になりました!</string>
@ -910,6 +916,7 @@
<string name="token_button_unavailability_reason_empty_balance_sell">売却できる資金がありません。アカウントに入金して、売却できるようにしてください。</string>
<string name="token_button_unavailability_reason_empty_balance_send">送金する資金がありません。アカウントに入金して、送金できるようにしてください。</string>
<string name="token_button_unavailability_reason_not_exchangeable">%sのスワップは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。</string>
<string name="token_button_unavailability_reason_out_of_date_balance">表示残高はキャッシュにより古くなっている可能性があります。</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">%sネットワーク上の保留中の取引が完了すると、資金の売却が可能になります。</string>
<string name="token_button_unavailability_reason_pending_transaction_send">ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。</string>
<string name="token_button_unavailability_reason_sell_unavailable">%sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。</string>
@ -970,6 +977,9 @@
<string name="user_wallet_list_rename_popup_title">ウォレット名の変更</string>
<string name="user_wallet_list_unlock_all">すべてのロックを解除</string>
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
<string name="visa_unlock_notification_button">ロック解除</string>
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>
<string name="visa_unlock_notification_title">ロック解除が必要</string>
<string name="wallet_balance_blockchain_unreachable_try_later">ブロックチェーンにアクセスできません。後でもう一度お試しください。</string>
<string name="wallet_balance_missing_derivation">カードまたはリングをスキャン</string>
<string name="wallet_been_activated_message">このウォレットはすでに有効化されています。\nあなたが行ったのでない場合は、サポートに連絡してください。\nTangemは、事前に生成されたアクセスコードと一緒にウォレットを販売することはありません。</string>
@ -1083,7 +1093,7 @@
<string name="warning_receive_blocked_hedera_token_association_required_message">トークンを受け取る前に、トークンを関連付ける必要があります。</string>
<string name="warning_rent_fee_title">ネットワーク使用料が必要です</string>
<string name="warning_seedphrase_action_required_title">対応が必要です</string>
<string name="warning_seedphrase_contacted_support">ウォレットを作成してから7日以内にアプリまたはEメール経由でサポートに連絡しましたか連絡したか不明な場合は、手順に従って完了してください。</string>
<string name="warning_seedphrase_contacted_support">ウォレットの作成後、7日以内にアプリ経由でサポートに連絡しましたか連絡した場合、あるいは連絡したかわからない場合は、「はい」をクリックして指示に従ってください。</string>
<string name="warning_seedphrase_issue_answer_no">ありがとうございます。準備完了です。これ以上の操作は必要ありません。</string>
<string name="warning_seedphrase_issue_answer_yes">Tangemの公式ウェブサイトに移動します。そちらの指示に従ってください。</string>
<string name="warning_seedphrase_issue_message">このアプリ経由でTangemサポートチームに直接連絡したことがありますか</string>

View file

@ -1111,6 +1111,7 @@
<string name="warning_rate_app_title">Подобається Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необхідно провести асоціацію токена, щоб мати можливість приймати його</string>
<string name="warning_rent_fee_title">Необхідна плата за оренду мережі</string>
<string name="warning_seedphrase_contacted_support">Чи зверталися в службу підтримки через додаток протягом 7 днів після створення гаманця? Якщо зверталися або не впевнені, натисніть «Так» і дотримуйтесь інструкцій.</string>
<string name="warning_seedphrase_issue_answer_no">Дякуємо! Все добре! Подальших дій не потрібно.</string>
<string name="warning_seedphrase_issue_answer_yes">Ви будете перенаправлені на офіційний сайт Tangem. Будь ласка, прочитайте та виконайте вказані там інструкції.</string>
<string name="warning_seedphrase_issue_message">Ви коли-небудь зверталися до служби підтримки Tangem безпосередньо з цього додатку?</string>

View file

@ -926,7 +926,7 @@
<string name="token_button_unavailability_reason_empty_balance_sell">You do not have funds to sell. Top up your account to be able to sell funds from it.</string>
<string name="token_button_unavailability_reason_empty_balance_send">You do not have funds to send. Top up your account to be able to send funds from it.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Swapping %s is not supported by current providers, but we are working to add more options.</string>
<string name="token_button_unavailability_reason_out_od_date_balance">The displayed balance might be outdated due to caching.</string>
<string name="token_button_unavailability_reason_out_of_date_balance">The displayed balance might be outdated due to caching.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the pending transaction(s) on the %s network is complete.</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Sending funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not supported by current providers, but we are working to add more options.</string>

View file

@ -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

View file

@ -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(),
)
}

View file

@ -144,10 +144,10 @@ internal class DefaultHotCryptoRepository(
return copy(
tokens = this.tokens
.applyTokensIdMigrations()
.filter { hotToken ->
!tokens.hasAlreadyAdded(hotToken) && userWallet.canHandleHotCrypto(hotToken)
}
.applyTokensIdMigrations(),
},
)
}

View file

@ -44,15 +44,15 @@ internal class DefaultPromoRepository(
}
override fun getStoryById(id: String): Flow<StoryContent?> = 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) {

View file

@ -18,7 +18,7 @@ class GetStoryContentUseCase(
.catch { emit(it.left()) }
.onEmpty { emit(null.right()) }
suspend fun invokeSync(id: String): Either<Throwable, StoryContent?> = Either.catch {
promoRepository.getStoryByIdSync(id)
suspend fun invokeSync(id: String, refresh: Boolean = false): Either<Throwable, StoryContent?> = Either.catch {
promoRepository.getStoryByIdSync(id, refresh)
}
}

View file

@ -18,7 +18,7 @@ interface PromoRepository {
// region Stories
fun getStoryById(id: String): Flow<StoryContent?>
suspend fun getStoryByIdSync(id: String): StoryContent?
suspend fun getStoryByIdSync(id: String, refresh: Boolean): StoryContent?
fun isReadyToShowStories(storyId: String): Flow<Boolean>

View file

@ -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,
-> ""
}
}

View file

@ -49,6 +49,14 @@ class FetchTokenListUseCase(
): Either<TokenListError, Unit> = either {
val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies)
invoke(userWalletId = userWalletId, currencies = currencies, mode = mode)
}
suspend operator fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
mode: RefreshMode = RefreshMode.NONE,
): Either<TokenListError, Unit> = either {
coroutineScope {
val fetchStatuses = async {
fetchNetworksStatuses(

View file

@ -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<TokenListError, TotalFiatBalance> {
return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId)
.transformLatest { maybeStatuses ->
val balance = createBalance(maybeStatuses)
emit(balance)
}
return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId).map(::createBalance)
}
private fun createBalance(
maybeStatuses: Lce<TokenListError, List<CryptoCurrencyStatus>>,
): Lce<TokenListError, TotalFiatBalance> = 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() },

View file

@ -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<TokenListError, List<CryptoCurrencyStatus>> = 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()) {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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<TokenActionsState.ActionState>) = 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()
}

View file

@ -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 = {},

View file

@ -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<MyPortfolioUM> {
@ -61,7 +60,11 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
),
isQuickActionsShown = false,
quickActions = PortfolioTokenUM.QuickActions(
actions = QuickActionUM.entries.toImmutableList(),
actions = persistentListOf(
QuickActionUM.Buy,
QuickActionUM.Exchange(showBadge = true),
QuickActionUM.Receive,
),
onQuickActionClick = {},
onQuickActionLongClick = {},
),

View file

@ -7,26 +7,30 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
@Immutable
internal enum class QuickActionUM(
internal sealed class QuickActionUM(
val title: TextReference,
val description: TextReference,
@DrawableRes val icon: Int,
val longClickAvailable: Boolean = false,
) {
Buy(
data object Buy : QuickActionUM(
title = resourceReference(R.string.common_buy),
description = resourceReference(R.string.buy_token_description),
icon = R.drawable.ic_plus_24,
),
Exchange(
)
data class Exchange(
val showBadge: Boolean,
) : QuickActionUM(
title = resourceReference(R.string.common_exchange),
description = resourceReference(R.string.exсhange_token_description),
icon = R.drawable.ic_exchange_vertical_24,
),
Receive(
)
data object Receive : QuickActionUM(
title = resourceReference(R.string.common_receive),
description = resourceReference(R.string.receive_token_description),
icon = R.drawable.ic_arrow_down_24,
longClickAvailable = true,
),
)
}

View file

@ -19,15 +19,4 @@ sealed class StoriesEvents(
WATCHED to watchCount,
),
)
data class Error(
val source: String,
val type: String,
) : StoriesEvents(
event = "Error",
params = mapOf(
AnalyticsParam.SOURCE to source,
AnalyticsParam.TYPE to type,
),
)
}

View file

@ -53,22 +53,10 @@ internal class StoriesModel @Inject constructor(
getStoryContentUseCase.invokeSync(params.storyId).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,
),
)
openScreen(hideStories = false) // Fallback to target screen
},
ifRight = { swapStory ->
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 {

View file

@ -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,
),
)

View file

@ -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,
),
)
}
},
)

View file

@ -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")
}
},