Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-04 18:38:23 +05:00
commit 253278ea49
30 changed files with 985 additions and 159 deletions

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="_warning_stelar_token_required_min_coin_reserve">Erforderliche Mindest-XLM-Reserve %1$s</string>
<string name="action_buttons_buy_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu.</string>
<string name="action_buttons_sell_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu.</string>
<string name="action_buttons_sell_navigation_bar_title">Verkaufen</string>
@ -65,6 +66,10 @@
<item quantity="one">%d Gerät</item>
<item quantity="other">%d Geräte</item>
</plurals>
<plurals name="card_label_token_count">
<item quantity="one">Token</item>
<item quantity="other">Tokens</item>
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte oder Ring zum Zurücksetzen von Zugangscodes auf anderen Karten oder Ringe in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte oder Ring nicht zurücksetzen kannst.</string>
<string name="card_settings_access_code_recovery_enabled_description">Ermöglicht die Verwendung dieser Karte oder Ring zum Zurücksetzen des Zugangscodes auf anderen Karten oder Ringe in dieser Brieftasche</string>
<string name="card_settings_access_code_recovery_title">Zugangspin resetten</string>
@ -110,6 +115,7 @@
<string name="common_cancel">Abbrechen</string>
<string name="common_change">Ändern</string>
<string name="common_choose_action">Aktion wählen</string>
<string name="common_choose_token">Token auswählen</string>
<string name="common_claim">Beanspruchen</string>
<string name="common_claim_rewards">Stakingbelohnungen beanspruchen</string>
<string name="common_close">Schließen</string>
@ -151,6 +157,7 @@
<string name="common_generate_addresses">Adressen synchronisieren</string>
<string name="common_go_to_provider">Zum Anbieter gehen</string>
<string name="common_go_to_token">Zum Token</string>
<string name="common_got_it">Verstanden</string>
<string name="common_hide">Ausblenden</string>
<string name="common_hour">stunde</string>
<string name="common_import">Importieren</string>
@ -205,6 +212,7 @@
<string name="common_submit">Einreichen</string>
<string name="common_success">Erfolgt</string>
<string name="common_support">Unterstützung</string>
<string name="common_supported_networks">Unterstützte Netzwerke</string>
<string name="common_swap">Tauschen</string>
<string name="common_terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
<string name="common_terms_of_use">Nutzungsbedingungen</string>
@ -264,6 +272,7 @@
<string name="details_row_subtitle_signed_hashes_format">%s Hasch</string>
<string name="details_row_title_cid">Geräte-ID</string>
<string name="details_row_title_contact_to_support">Kontakt zum Support</string>
<string name="details_row_title_contact_to_support_chat">Support-Chat öffnen</string>
<string name="details_row_title_create_backup">Weitere Karten oder Ringe verknüpfen</string>
<string name="details_row_title_currency">App Währung</string>
<string name="details_row_title_flip_to_hide">Flipp um Guthaben auszublenden</string>
@ -701,10 +710,13 @@
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="push_notifications_more_info">Mehr Infos</string>
<string name="push_notifications_permission_alert_description">Du kannst Benachrichtigungen für Tangem in den Einstellungen aktivieren.</string>
<string name="push_notifications_permission_alert_negative_button">Später aktivieren</string>
<string name="push_notifications_permission_alert_positive_button">Einstellungen</string>
<string name="push_notifications_permission_alert_title">Benachrichtigungen aktivieren</string>
<string name="push_transactions_notifications_description">Erhalte Benachrichtigungen über eingehende Transaktionen der unterstützten Netzwerke</string>
<string name="push_transactions_notifications_title">Transaktionsbenachrichtigungen</string>
<string name="qr_scanner_camera_denied_gallery_button">Wählen aus der Galerie aus</string>
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
@ -928,7 +940,7 @@
<string name="staking_notification_restake_text">Mit Restake kannst Du Dein Guthaben von einem Validator zu einem anderen verschieben, ohne dass Du den Stake aufheben musst.</string>
<string name="staking_notification_stake_entire_balance_text">Du bist dabei, Dein gesamtes Guthaben zu staken. Wir empfehlen, einen kleinen Betrag übrig zu lassen, um die Netzwerkgebühren für die Aufhebung des Stakes oder das Einfordern von Prämien abzudecken.</string>
<string name="staking_notification_ton_activate_account">Um mit dem Staking bei TON zu beginnen, führe zunächst eine ausgehende Transaktion in beliebiger Höhe durch dadurch wird Deine Wallet aktiviert.</string>
<string name="staking_notification_ton_extra_reserve_info">Zusätzlich zur Netzwerkgebühr werden bis zu 0,2 TON benötigt, um alle Schritte der Transaktion abzuschließen. Nicht genutzte Beträge werden nach Abschluss der Transaktion zurückerstattet.</string>
<string name="staking_notification_ton_extra_reserve_info">Für den Abschluss der Transaktion können zusätzlich zur Netzwerkgebühr bis zu 0,2 TON erforderlich sein. Nicht genutzte Beträge werden zurückerstattet.</string>
<string name="staking_notification_ton_extra_reserve_is_required">Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 TON erforderlich. Bitte lade Dein Guthaben auf.</string>
<string name="staking_notification_ton_extra_reserve_title">0,2 Ton reserviert</string>
<string name="staking_notification_ton_have_to_unstake_all_text">Durch diese Aktion werden andere Positionen geschlossen oder gemäß den Netzwerkregeln in den Auszahlungsstatus versetzt.</string>
@ -1071,6 +1083,8 @@
<string name="transaction_history_transaction_from_address">von: %s</string>
<string name="transaction_history_transaction_to_address">zu: %s</string>
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transaction_notifications_warning_description">Benachrichtigungen sind aktiviert, funktionieren aber erst, wenn Du Benachrichtigungen in Deinen Geräteeinstellungen zulässt.</string>
<string name="transaction_notifications_warning_title">Transaktionsbenachrichtigungen</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken.</string>
@ -1087,10 +1101,13 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet.</string>
<string name="twins_scan_twin_with_number">Tippe auf die Doppelkarte oder Ring mit der Nummer %s und entferne sie erst am Ende des Vorgangs.</string>
<string name="unexpected_error_description">Bitte versuche es später noch einmal. Sollte das Problem weiterhin bestehen, wende Dich bitte an den Support.</string>
<string name="unexpected_error_title">Etwas ist schiefgelaufen!</string>
<string name="universal_error">Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support.</string>
<string name="unlock_wallet_description_full">Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten.</string>
<string name="unsupported_wc_version">Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen.</string>
<string name="user_push_notification_agreement_argument_one">Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten</string>
<string name="user_push_notification_agreement_argument_three">Genehmigung für Transaktionen anfordern</string>
<string name="user_push_notification_agreement_argument_two">Sei der Erste, der von neuen Aktionen erfährt</string>
<string name="user_push_notification_agreement_header">Möchtest du Push-Benachrichtigungen verwenden?</string>
<string name="user_wallet_list_add_button">Neues Wallet hinzufügen</string>
@ -1170,7 +1187,7 @@
<string name="wallet_promo_banner_description">Greife auf 13.000+ Kryptowährungen zu. Kaufe, verkaufe, tausche und stake Sie mit einem einzigen Fingertipp.\nVerbinde bis zu drei Karten für ein Backup.</string>
<string name="wallet_promo_banner_title">Entdecke die Tangem Wallet</string>
<string name="wallet_settings_push_notifications_description">Bleib stets über eingehende Wallet-Transaktionen und Tangem-Updates auf dem Laufenden.</string>
<string name="wallet_settings_push_notifications_title">Push-Benachrichtigungen</string>
<string name="wallet_settings_push_notifications_title">Transaktionsbenachrichtigungen</string>
<string name="wallet_settings_title">Wallet-Einstellungen</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Verwende %s oder scanne eine Karte oder Ring, um den Zugriff auf deine Wallet freizuschalten.</string>
@ -1240,6 +1257,7 @@
<string name="warning_rate_app_message">Deine Bewertung motiviert uns, die Tangem Wallet noch besser zu machen.</string>
<string name="warning_rate_app_title">Gefällt dir Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Du musst deinen Token zuordnen, bevor du Token erhalten kannst</string>
<string name="warning_receive_blocked_stelar_token_trustline_required_message">Du musst die trustline öffnen, bevor Du Token erhältst</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 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>
@ -1255,6 +1273,9 @@
<string name="warning_some_networks_unreachable_message">Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut.</string>
<string name="warning_some_networks_unreachable_title">Einige Netzwerke sind nicht erreichbar</string>
<string name="warning_some_token_balances_not_updated">Einige Token-Guthaben konnten nicht aktualisiert werden</string>
<string name="warning_stellar_token_trustline_button_title">Trustline öffnen</string>
<string name="warning_stellar_token_trustline_subtiile">Trustline-Untermenü öffnen</string>
<string name="warning_stellar_token_trustline_tiile">Trustline-Titel öffnen</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>

View file

@ -110,6 +110,7 @@
<string name="common_cancel">Cancelar</string>
<string name="common_change">Cambie</string>
<string name="common_choose_action">Elige una acción</string>
<string name="common_choose_token">Elige token</string>
<string name="common_claim">Reclamar</string>
<string name="common_claim_rewards">Reclame recompensas</string>
<string name="common_close">Cerrar</string>

View file

@ -110,6 +110,7 @@
<string name="common_cancel">Annuler</string>
<string name="common_change">Changez</string>
<string name="common_choose_action">Choisissez une action</string>
<string name="common_choose_token">Choisir le jeton</string>
<string name="common_claim">Réclamer</string>
<string name="common_claim_rewards">Réclamez des récompenses</string>
<string name="common_close">Fermer</string>

View file

@ -112,6 +112,7 @@
<string name="common_cancel">キャンセル</string>
<string name="common_change">変更</string>
<string name="common_choose_action">アクションを選択</string>
<string name="common_choose_token">トークンを選択</string>
<string name="common_claim">請求</string>
<string name="common_claim_rewards">報酬を受け取る</string>
<string name="common_close">閉じる</string>

View file

@ -112,6 +112,7 @@
<string name="common_cancel">Отмена</string>
<string name="common_change">Изменить</string>
<string name="common_choose_action">Выберите действие</string>
<string name="common_choose_token">Выберите токен</string>
<string name="common_claim">Получить</string>
<string name="common_claim_rewards">Вывести награду</string>
<string name="common_close">Закрыть</string>

View file

@ -112,6 +112,7 @@
<string name="common_cancel">Скасувати</string>
<string name="common_change">Змінити</string>
<string name="common_choose_action">Виберіть дію</string>
<string name="common_choose_token">Оберіть токен</string>
<string name="common_claim">Отримати</string>
<string name="common_claim_rewards">Отримати винагороди</string>
<string name="common_close">Закрити</string>

View file

@ -50,6 +50,7 @@
<string name="common_buy">購買</string>
<string name="common_camera_denied_alert_message">您尚未授予相機訪問權限,請更改您的隱私設置</string>
<string name="common_cancel">删除</string>
<string name="common_choose_token">選擇代幣</string>
<string name="common_close">關閉</string>
<string name="common_continue">繼續</string>
<string name="common_copy">複製</string>

View file

@ -115,6 +115,7 @@
<string name="common_cancel">Cancel</string>
<string name="common_change">Change</string>
<string name="common_choose_action">Choose action</string>
<string name="common_choose_token">Choose token</string>
<string name="common_claim">Claim</string>
<string name="common_claim_rewards">Claim rewards</string>
<string name="common_close">Close</string>
@ -871,6 +872,8 @@
<string name="send_validation_invalid_amount">Invalid amount</string>
<string name="send_validation_invalid_fee">Fee exceeds balance</string>
<string name="send_validation_invalid_total">Total amount exceeds balance</string>
<string name="send_with_swap_notification_text">Send any token, and well convert it on the way. Your recipient gets exactly what they need—seamlessly.</string>
<string name="send_with_swap_title">Send with swap</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card or ring you want to set up.</string>
<string name="settings_forget_wallet">Forget wallet</string>

View file

@ -1,5 +1,6 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
@ -8,4 +9,7 @@ dependencies {
/* Domain */
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
/** Other */
implementation(deps.kotlin.serialization)
}

View file

@ -2,7 +2,9 @@ package com.tangem.domain.managetokens.model
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import kotlinx.serialization.Serializable
@Serializable
sealed class ManagedCryptoCurrency {
abstract val id: ID
@ -10,6 +12,7 @@ sealed class ManagedCryptoCurrency {
abstract val symbol: String
abstract val iconUrl: String?
@Serializable
sealed class Custom : ManagedCryptoCurrency() {
abstract val currencyId: CryptoCurrency.ID
@ -18,6 +21,7 @@ sealed class ManagedCryptoCurrency {
override val id: ID
get() = ID(currencyId.value)
@Serializable
data class Token(
override val currencyId: CryptoCurrency.ID,
override val name: String,
@ -28,6 +32,7 @@ sealed class ManagedCryptoCurrency {
val decimals: Int,
) : Custom()
@Serializable
data class Coin(
override val currencyId: CryptoCurrency.ID,
override val name: String,
@ -37,6 +42,7 @@ sealed class ManagedCryptoCurrency {
) : Custom()
}
@Serializable
data class Token(
override val id: ID,
override val name: String,
@ -50,8 +56,10 @@ sealed class ManagedCryptoCurrency {
}
@JvmInline
@Serializable
value class ID(val value: String)
@Serializable
sealed class SourceNetwork {
abstract val network: Network
@ -77,12 +85,14 @@ sealed class ManagedCryptoCurrency {
}
}
@Serializable
data class Main(
override val network: Network,
override val decimals: Int,
val isL2Network: Boolean,
) : SourceNetwork()
@Serializable
data class Default(
override val network: Network,
override val decimals: Int,

View file

@ -10,7 +10,9 @@ android {
dependencies {
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.manageTokens.models)
/* Project - Core */
implementation(projects.core.ui)

View file

@ -0,0 +1,21 @@
package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
interface ChooseManagedTokensComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val source: Source,
)
enum class Source {
SendViaSwap,
}
interface Factory : ComponentFactory<Params, ChooseManagedTokensComponent>
}

View file

@ -4,4 +4,5 @@ enum class ManageTokensSource(val analyticsName: String) {
STORIES(analyticsName = "Stories"),
ONBOARDING(analyticsName = "Onboarding"),
SETTINGS(analyticsName = "Settings"),
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
}

View file

@ -18,9 +18,10 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.common.routing)
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.common.routing)
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.card)

View file

@ -0,0 +1,72 @@
package com.tangem.features.managetokens.choosetoken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel
import com.tangem.features.managetokens.choosetoken.ui.ChooseManagedTokenContent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultChooseManagedTokensComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: ChooseManagedTokensComponent.Params,
) : ChooseManagedTokensComponent, AppComponentContext by context {
private val model: ChooseManagedTokensModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = ChooseManageTokensBottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
ChooseManagedTokenContent(
state = uiState,
)
bottomSheet.child?.instance?.BottomSheet()
}
@Suppress("UnusedPrivateMember")
private fun bottomSheetChild(
config: ChooseManageTokensBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
else -> getStubComponent()
}
private fun getStubComponent() = StubComponent()
class StubComponent : ComposableBottomSheetComponent {
override fun dismiss() {}
@Composable
override fun BottomSheet() {
/* no-op */
}
}
@AssistedFactory
interface Factory : ChooseManagedTokensComponent.Factory {
override fun create(
context: AppComponentContext,
params: ChooseManagedTokensComponent.Params,
): DefaultChooseManagedTokensComponent
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.managetokens.choosetoken.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.managetokens.choosetoken.DefaultChooseManagedTokensComponent
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@InstallIn(ModelComponent::class)
@Module
internal interface ChooseManagedTokensModule {
@Binds
@IntoMap
@ClassKey(ChooseManagedTokensModel::class)
fun provideChooseManagedTokensModel(impl: ChooseManagedTokensModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal interface ChooseManagedTokensModuleBinds {
@Binds
@Singleton
fun provideChooseManagedTokensComponent(
impl: DefaultChooseManagedTokensComponent.Factory,
): ChooseManagedTokensComponent.Factory
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.managetokens.choosetoken.entity
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
internal sealed class ChooseManageTokensBottomSheetConfig {
@Serializable
data class SwapTokensBottomSheetConfig(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val token: ManagedCryptoCurrency.Token,
) : ChooseManageTokensBottomSheetConfig()
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.managetokens.choosetoken.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
internal data class ChooseManagedTokenUM(
val notificationUM: NotificationUM?,
val readContent: ManageTokensUM.ReadContent,
)

View file

@ -0,0 +1,270 @@
package com.tangem.features.managetokens.choosetoken.model
import androidx.annotation.StringRes
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.features.managetokens.utils.list.getLoadingItems
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.isNotEmpty
@ModelScoped
internal class ChooseManagedTokensModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val uiMessageSender: UiMessageSender,
paramsContainer: ParamsContainer,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
) : Model() {
private val params: ChooseManagedTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create(
onCurrencySelect = { token ->
bottomSheetNavigation.activate(
ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig(
userWalletId = params.userWalletId,
initialCurrency = params.initialCurrency,
token = token,
),
)
},
)
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<ChooseManagedTokenUM>
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
init {
manageTokensListManager.uiItems
.onEach { items -> updateItems(items) }
.launchIn(modelScope)
manageTokensListManager.paginationStatus
.onEach { status -> updatePaginationStatus(status) }
.launchIn(modelScope)
observeSearchQueryChanges()
modelScope.launch {
manageTokensListManager.launchPagination(source = ManageTokensSource.SEND_VIA_SWAP, userWalletId = null)
}
}
private fun createReadContentModel(): ChooseManagedTokenUM {
return ChooseManagedTokenUM(
notificationUM = getNotification(),
readContent = ManageTokensUM.ReadContent(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_choose_token),
onBackButtonClick = router::pop,
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = ::searchCurrencies,
isActive = false,
onActiveChange = ::toggleSearchBar,
),
loadMore = ::loadMoreItems,
),
)
}
private fun getNotification(): NotificationUM? {
return when (params.source) {
Source.SendViaSwap -> ChooseManagedTokensNotificationUM.SendViaSwap(
onCloseClick = ::removeNotification,
)
}
}
private fun removeNotification() {
uiState.update {
it.copy(
notificationUM = null,
)
}
}
@OptIn(FlowPreview::class)
private fun observeSearchQueryChanges() {
uiState
.distinctUntilChanged { old, new ->
// It's also used to skip search activation to avoid searching an empty query
old.readContent.search.query == new.readContent.search.query && new.readContent.search.isActive
}
.transform { state ->
val query = state.readContent.search.query
if (state.readContent.search.isActive) {
emit(query)
}
}
.sample(periodMillis = 1_000)
.onEach { query -> manageTokensListManager.search(userWalletId = null, query = query) }
.launchIn(modelScope)
}
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(items = items),
)
}
}
private fun consumeScrollToTopEvent() {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(scrollToTop = consumedEvent()),
)
}
}
private fun updatePaginationStatus(status: PaginationStatus<*>) {
uiState.update { state ->
val readContent = state.readContent
when (status) {
is PaginationStatus.None,
is PaginationStatus.InitialLoading,
-> {
if (readContent.search.isActive) {
state.copy(
readContent = readContent.copy(items = getLoadingItems()),
)
} else {
state.copy(
readContent = readContent.copy(
items = getLoadingItems(),
isInitialBatchLoading = true,
),
)
}
}
is PaginationStatus.NextBatchLoading -> state.copy(
readContent = readContent.copy(isNextBatchLoading = true),
)
is PaginationStatus.InitialLoadingError -> {
val message = SnackbarMessage(
message = status.throwable.localizedMessage
?.let(::stringReference)
?: resourceReference(R.string.common_error),
)
uiMessageSender.send(message)
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
),
)
}
is PaginationStatus.Paginating -> {
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
Timber.e(fetchError.throwable)
}
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
scrollToTop = if (readContent.isInitialBatchLoading && readContent.items.isNotEmpty()) {
triggeredEvent(
data = Unit,
onConsume = ::consumeScrollToTopEvent,
)
} else {
readContent.scrollToTop
},
),
)
}
is PaginationStatus.EndOfPagination -> {
state.copy(
readContent = readContent.copy(
isInitialBatchLoading = false,
isNextBatchLoading = false,
),
)
}
}
}
}
private fun searchCurrencies(query: String) {
uiState.update { state ->
state.copy(
readContent = state.readContent.copy(
search = state.readContent.search.copy(
query = query,
isActive = true,
),
),
)
}
}
private fun toggleSearchBar(isActive: Boolean) {
uiState.update { state ->
@StringRes val placeholderTextRes = if (isActive) {
R.string.manage_tokens_search_placeholder
} else {
R.string.common_search
}
state.copy(
readContent = state.readContent.copy(
search = state.readContent.search.copy(
placeholderText = resourceReference(placeholderTextRes),
isActive = isActive,
),
),
)
}
}
private fun loadMoreItems(): Boolean {
val state = uiState.value
if (state.readContent.isInitialBatchLoading || state.readContent.isNextBatchLoading) return false
modelScope.launch {
manageTokensListManager.loadMore(userWalletId = null, query = state.readContent.search.query)
}
return true
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.managetokens.choosetoken.model
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.managetokens.impl.R
internal object ChooseManagedTokensNotificationUM {
data class SendViaSwap(
val onCloseClick: () -> Unit,
) : NotificationUM.Info(
title = resourceReference(R.string.send_with_swap_title),
subtitle = resourceReference(R.string.send_with_swap_notification_text),
iconResId = R.drawable.ic_exchange_horizontal_24,
onCloseClick = onCloseClick,
)
}

View file

@ -0,0 +1,234 @@
package com.tangem.features.managetokens.choosetoken.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensNotificationUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
internal fun ChooseManagedTokenContent(state: ChooseManagedTokenUM, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.tertiary,
contentWindowInsets = WindowInsetsZero,
topBar = {
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
topBar = state.readContent.topBar,
search = state.readContent.search,
)
},
content = { innerPadding ->
Content(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = state,
)
},
)
}
@Composable
private fun Content(state: ChooseManagedTokenUM, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Box(modifier = modifier) {
Currencies(
modifier = Modifier.fillMaxSize(),
listState = listState,
notificationUM = state.notificationUM,
items = state.readContent.items,
showLoadingItem = state.readContent.isNextBatchLoading,
onLoadMore = state.readContent.loadMore,
)
}
EventEffect(event = state.readContent.scrollToTop) {
listState.animateScrollToItem(index = 0)
}
}
@Composable
private fun Currencies(
listState: LazyListState,
notificationUM: NotificationUM?,
items: ImmutableList<CurrencyItemUM>,
showLoadingItem: Boolean,
onLoadMore: () -> Boolean,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) {
WindowInsets.systemBars.getBottom(density = this).toDp()
}
LazyColumn(
modifier = modifier.padding(horizontal = 16.dp),
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing76 + bottomBarHeight,
),
) {
if (notificationUM?.config != null) {
item(key = "notification_key") {
Notification(
config = notificationUM.config,
iconTint = TangemTheme.colors.icon.accent,
modifier = Modifier
.animateItem()
.padding(bottom = 12.dp),
)
}
}
contentItems(items)
if (showLoadingItem) {
item(key = "loading_item") {
ProgressIndicator(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
}
}
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = onLoadMore,
)
}
private fun LazyListScope.contentItems(items: ImmutableList<CurrencyItemUM>) {
itemsIndexed(
items = items,
key = { index, item -> item.id.value },
) { index, item ->
when (item) {
is CurrencyItemUM.Basic -> {
ChainRow(
model = with(item) {
ChainRowUM(
name = name,
type = symbol,
icon = icon,
showCustom = false,
)
},
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.clickable(onClick = item.onExpandClick)
.background(TangemTheme.colors.background.action),
)
}
is CurrencyItemUM.Loading -> {
LoadingItem(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.fillMaxWidth()
.background(TangemTheme.colors.background.action),
)
}
is CurrencyItemUM.SearchNothingFound -> {
SearchNothingFoundText(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
addDefaultPadding = false,
lastIndex = items.lastIndex,
)
.background(TangemTheme.colors.background.action)
.fillParentMaxSize(),
)
}
else -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ChooseManagedTokenContent_Preview() {
TangemThemePreview {
ChooseManagedTokenContent(
state = ChooseManagedTokenUM(
notificationUM = ChooseManagedTokensNotificationUM.SendViaSwap({}),
readContent = ManageTokensUM.ReadContent(
popBack = {},
isInitialBatchLoading = false,
isNextBatchLoading = false,
items = buildList {
repeat(10) {
add(
CurrencyItemUM.Basic(
id = ManagedCryptoCurrency.ID(
value = "ID+$it",
),
name = "Bitcoin",
symbol = "BTC",
icon = CurrencyIconState.Loading,
networks = CurrencyItemUM.Basic.NetworksUM.Collapsed,
onExpandClick = {},
),
)
}
}.toPersistentList(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_choose_token),
onBackButtonClick = {},
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
loadMore = { true },
),
),
)
}
}
// endregion

View file

@ -46,16 +46,18 @@ import javax.inject.Inject
internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: ManageTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create()
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState(params.userWalletId))
val bottomSheetNavigation: SlotNavigation<ManageTokensBottomSheetConfig> = SlotNavigation()

View file

@ -40,16 +40,18 @@ import javax.inject.Inject
@ModelScoped
internal class OnboardingManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create()
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
val returnToParentComponentFlow = MutableSharedFlow<Unit>()

View file

@ -32,8 +32,6 @@ import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButton
@ -41,8 +39,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
@ -51,7 +47,6 @@ import com.tangem.core.ui.components.rows.ChainRowContainer
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.event.EventEffect
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.haptic.TangemHapticEffect
@ -66,14 +61,13 @@ import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import kotlinx.collections.immutable.ImmutableList
private const val CHEVRON_ROTATION_EXPANDED = 180f
private const val CHEVRON_ROTATION_COLLAPSED = 0f
private const val LOAD_ITEMS_BUFFER = 10
internal const val LOAD_ITEMS_BUFFER = 10
@Composable
internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) {
@ -85,7 +79,9 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
contentWindowInsets = WindowInsetsZero,
topBar = {
ManageTokensTopBar(
modifier = Modifier.statusBarsPadding(),
modifier = Modifier
.statusBarsPadding()
.background(TangemTheme.colors.background.primary),
topBar = state.topBar,
search = state.search,
)
@ -116,31 +112,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
)
}
@Composable
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
if (topBar != null) {
TangemTopAppBar(
title = topBar.title.resolveReference(),
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
endButton = when (topBar) {
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
is ManageTokensTopBarUM.ReadContent -> null
},
)
}
SearchBar(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = search,
)
}
}
@Composable
private fun SaveChangesButton(
isVisible: Boolean,
@ -263,7 +234,7 @@ internal fun Currencies(
}
@Composable
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
fun SearchNothingFoundText(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
@ -277,7 +248,7 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
}
@Composable
private fun ProgressIndicator(modifier: Modifier = Modifier) {
fun ProgressIndicator(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(color = TangemTheme.colors.background.primary),
contentAlignment = Alignment.Center,
@ -287,7 +258,7 @@ private fun ProgressIndicator(modifier: Modifier = Modifier) {
}
@Composable
private fun LoadingItem(modifier: Modifier = Modifier) {
fun LoadingItem(modifier: Modifier = Modifier) {
ChainRowContainer(
modifier = modifier,
icon = {

View file

@ -0,0 +1,39 @@
package com.tangem.features.managetokens.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
@Composable
internal fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
if (topBar != null) {
TangemTopAppBar(
title = topBar.title.resolveReference(),
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
endButton = when (topBar) {
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
is ManageTokensTopBarUM.ReadContent -> null
},
)
}
SearchBar(
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
state = search,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}

View file

@ -2,11 +2,14 @@ package com.tangem.features.managetokens.utils.list
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.*
@ -16,6 +19,7 @@ import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.ui.toggleExpanded
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
@ -24,6 +28,9 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,11 +39,9 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class ManageTokensListManager @Inject constructor(
@Suppress("LongParameterList", "LargeClass")
internal class ManageTokensListManager @AssistedInject constructor(
private val getManagedTokensUseCase: GetManagedTokensUseCase,
private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
@ -45,7 +50,8 @@ internal class ManageTokensListManager @Inject constructor(
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
clipboardManager: ClipboardManager,
private val clipboardManager: ClipboardManager,
@Assisted private val onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {},
) : ManageTokensUiActions {
private lateinit var scope: CoroutineScope
@ -67,7 +73,6 @@ internal class ManageTokensListManager @Inject constructor(
actions = this,
scopeProvider = Provider { scope },
sourceProvider = Provider { source },
clipboardManager = clipboardManager,
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
@ -186,6 +191,14 @@ internal class ManageTokensListManager @Inject constructor(
}
}
override fun onTokenClick(currency: ManagedCryptoCurrency.Token) {
if (source == ManageTokensSource.SEND_VIA_SWAP) {
onCurrencySelect(currency)
} else {
toggleCurrencyNetworksVisibility(currency)
}
}
override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.addCurrency(currency, network)
@ -298,4 +311,168 @@ internal class ManageTokensListManager @Inject constructor(
null
}
}
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
dispatchers.default,
) {
state.update { batches ->
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
val currencyBatch = batches.currencyBatches[batchIndex]
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
val uiBatch = batches.uiBatches[batchIndex]
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
},
onLongTap = ::copyContractAddress,
)
batches.updateUiBatchesItem(
indexToBatch = batchIndex to uiBatch,
indexToItem = currencyIndex to updatedUiItem,
)
}
}
private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) {
if (source is ManagedCryptoCurrency.SourceNetwork.Default) {
clipboardManager.setText(text = source.contractAddress, isSensitive = false)
showSnackbarMessage(resourceReference(R.string.contract_address_copied_message))
}
}
private fun showSnackbarMessage(messageText: TextReference) {
val message = SnackbarMessage(message = messageText)
messageSender.send(message)
}
private fun selectNetwork(
batchKey: Int,
currency: ManagedCryptoCurrency,
source: ManagedCryptoCurrency.SourceNetwork,
isSelected: Boolean,
) = scope.launch(dispatchers.default) {
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
val userWalletId = state.value.userWalletId
val unsupportedState = userWalletId?.let { checkCurrencyUnsupportedState(it, source) }
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
addCurrency(batchKey, currency, source.network)
}
} else {
if (checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
removeCurrency(batchKey, currency, source.network)
},
)
} else {
removeCurrency(batchKey, currency, source.network)
}
}
}
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
val message = DialogMessage(
title = resourceReference(R.string.common_warning),
message = when (unsupportedState) {
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
},
)
messageSender.send(message)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val userWalletId = state.value.userWalletId
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
false
} else {
checkHasLinkedTokens(userWalletId, network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(
currency.name,
currency.symbol,
network.name,
),
),
)
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(R.string.token_details_hide_alert_message),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
.takeIf { it != -1 }
?: error("Currency with currency '$id' not found in batch #$key")
}
@AssistedFactory
interface Factory {
fun create(onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {}): ManageTokensListManager
}
}

View file

@ -7,6 +7,8 @@ import com.tangem.domain.wallets.models.UserWalletId
internal interface ManageTokensUiActions {
fun onTokenClick(currency: ManagedCryptoCurrency.Token)
fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)

View file

@ -1,21 +1,16 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toUiModel
import com.tangem.features.managetokens.utils.ui.toggleExpanded
import com.tangem.features.managetokens.utils.ui.update
import com.tangem.pagination.Batch
import com.tangem.utils.Provider
@ -25,7 +20,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@ -36,7 +34,6 @@ internal class ManageTokensUiManager(
private val scopeProvider: Provider<CoroutineScope>,
private val sourceProvider: Provider<ManageTokensSource>,
private val actions: ManageTokensUiActions,
private val clipboardManager: ClipboardManager,
) {
private val scope: CoroutineScope
@ -72,7 +69,7 @@ internal class ManageTokensUiManager(
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
onTokenClick = actions::onTokenClick,
)
},
)
@ -96,7 +93,7 @@ internal class ManageTokensUiManager(
item.toUiModel(
isEditable = canEditItems,
onRemoveCustomCurrencyClick = ::removeCustomCurrency,
onExpandNetworksClick = ::toggleCurrencyNetworksVisibility,
onTokenClick = actions::onTokenClick,
)
} else {
previousUiItem.update(item)
@ -123,97 +120,6 @@ internal class ManageTokensUiManager(
)
}
private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch(
dispatchers.default,
) {
state.update { batches ->
val batchIndex = batches.batchIndexByCurrencyId(currency.id)
val currencyBatch = batches.currencyBatches[batchIndex]
val currencyIndex = currencyBatch.currencyIndexById(currency.id)
val uiBatch = batches.uiBatches[batchIndex]
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected)
},
onLongTap = ::copyContractAddress,
)
batches.updateUiBatchesItem(
indexToBatch = batchIndex to uiBatch,
indexToItem = currencyIndex to updatedUiItem,
)
}
}
private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) {
if (source is ManagedCryptoCurrency.SourceNetwork.Default) {
clipboardManager.setText(text = source.contractAddress, isSensitive = false)
showSnackbarMessage(resourceReference(R.string.contract_address_copied_message))
}
}
private fun showSnackbarMessage(messageText: TextReference) {
val message = SnackbarMessage(message = messageText)
messageSender.send(message)
}
private fun selectNetwork(
batchKey: Int,
currency: ManagedCryptoCurrency,
source: ManagedCryptoCurrency.SourceNetwork,
isSelected: Boolean,
) = scope.launch(dispatchers.default) {
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
val userWalletId = state.value.userWalletId
val unsupportedState = userWalletId?.let { actions.checkCurrencyUnsupportedState(it, source) }
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
actions.addCurrency(batchKey, currency, source.network)
}
} else {
if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
actions.removeCurrency(batchKey, currency, source.network)
},
)
} else {
actions.removeCurrency(batchKey, currency, source.network)
}
}
}
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
val message = DialogMessage(
title = resourceReference(R.string.common_warning),
message = when (unsupportedState) {
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
},
)
messageSender.send(message)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
@ -274,11 +180,4 @@ internal class ManageTokensUiManager(
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
.takeIf { it != -1 }
?: error("Currency with currency '$id' not found in batch #$key")
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
internal fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
return List(size = 10) { index ->
CurrencyItemUM.Loading(index)
}.toPersistentList()
}

View file

@ -11,11 +11,11 @@ import com.tangem.features.managetokens.utils.ui.getIconRes
internal fun ManagedCryptoCurrency.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
onTokenClick: (ManagedCryptoCurrency.Token) -> Unit,
onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit,
): CurrencyItemUM = when (this) {
is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick)
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick)
is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onTokenClick)
}
private fun ManagedCryptoCurrency.Custom.toUiModel(
@ -53,7 +53,7 @@ private fun ManagedCryptoCurrency.Custom.toUiModel(
private fun ManagedCryptoCurrency.Token.toUiModel(
isEditable: Boolean,
onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit,
onTokenClick: (ManagedCryptoCurrency.Token) -> Unit,
): CurrencyItemUM {
val background = TangemColorPalette.Black
@ -71,7 +71,7 @@ private fun ManagedCryptoCurrency.Token.toUiModel(
),
networks = NetworksUM.Collapsed,
onExpandClick = {
onExpandNetworksClick(this)
onTokenClick(this)
},
)
}