diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 0fe3d3cfba..a6b74d1e24 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,5 +1,6 @@ + Erforderliche Mindest-XLM-Reserve %1$s Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu. Verkaufen @@ -65,6 +66,10 @@ %d Gerät %d Geräte + + Token + Tokens + Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte oder Ring zum Zurücksetzen von Zugangscodes auf anderen Karten oder Ringe in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte oder Ring nicht zurücksetzen kannst. Ermöglicht die Verwendung dieser Karte oder Ring zum Zurücksetzen des Zugangscodes auf anderen Karten oder Ringe in dieser Brieftasche Zugangspin resetten @@ -110,6 +115,7 @@ Abbrechen Ändern Aktion wählen + Token auswählen Beanspruchen Stakingbelohnungen beanspruchen Schließen @@ -151,6 +157,7 @@ Adressen synchronisieren Zum Anbieter gehen Zum Token + Verstanden Ausblenden stunde Importieren @@ -205,6 +212,7 @@ Einreichen Erfolgt Unterstützung + Unterstützte Netzwerke Tauschen Allgemeine Geschäftsbedingungen Nutzungsbedingungen @@ -264,6 +272,7 @@ %s Hasch Geräte-ID Kontakt zum Support + Support-Chat öffnen Weitere Karten oder Ringe verknüpfen App Währung Flipp um Guthaben auszublenden @@ -701,10 +710,13 @@ Nach Guthaben Token organisieren Gruppe löschen + Mehr Infos Du kannst Benachrichtigungen für Tangem in den Einstellungen aktivieren. Später aktivieren Einstellungen Benachrichtigungen aktivieren + Erhalte Benachrichtigungen über eingehende Transaktionen der unterstützten Netzwerke + Transaktionsbenachrichtigungen Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -928,7 +940,7 @@ Mit Restake kannst Du Dein Guthaben von einem Validator zu einem anderen verschieben, ohne dass Du den Stake aufheben musst. 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. 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. - 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. + 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. Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 TON erforderlich. Bitte lade Dein Guthaben auf. 0,2 Ton reserviert Durch diese Aktion werden andere Positionen geschlossen oder gemäß den Netzwerkregeln in den Auszahlungsstatus versetzt. @@ -1071,6 +1083,8 @@ von: %s zu: %s Validierer: %s + Benachrichtigungen sind aktiviert, funktionieren aber erst, wenn Du Benachrichtigungen in Deinen Geräteeinstellungen zulässt. + Transaktionsbenachrichtigungen Minimum %s Der Mindesttransaktionsbetrag beträgt %1$s. 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. @@ -1087,10 +1101,13 @@ Tangem Twin Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte oder Ring mit der Nummer %s und entferne sie erst am Ende des Vorgangs. + Bitte versuche es später noch einmal. Sollte das Problem weiterhin bestehen, wende Dich bitte an den Support. + Etwas ist schiefgelaufen! Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten + Genehmigung für Transaktionen anfordern Sei der Erste, der von neuen Aktionen erfährt Möchtest du Push-Benachrichtigungen verwenden? Neues Wallet hinzufügen @@ -1170,7 +1187,7 @@ 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. Entdecke die Tangem Wallet Bleib stets über eingehende Wallet-Transaktionen und Tangem-Updates auf dem Laufenden. - Push-Benachrichtigungen + Transaktionsbenachrichtigungen Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte oder Ring, um den Zugriff auf deine Wallet freizuschalten. @@ -1240,6 +1257,7 @@ Deine Bewertung motiviert uns, die Tangem Wallet noch besser zu machen. Gefällt dir Tangem? Du musst deinen Token zuordnen, bevor du Token erhalten kannst + Du musst die trustline öffnen, bevor Du Token erhältst Netzmietgebühr erforderlich Handlungsbedarf 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. @@ -1255,6 +1273,9 @@ Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut. Einige Netzwerke sind nicht erreichbar Einige Token-Guthaben konnten nicht aktualisiert werden + Trustline öffnen + Trustline-Untermenü öffnen + Trustline-Titel öffnen Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. Nur für Testzwecke Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index ab1c626b51..659151f3a2 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -110,6 +110,7 @@ Cancelar Cambie Elige una acción + Elige token Reclamar Reclame recompensas Cerrar diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index b7102eaf1b..1725c7e069 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -110,6 +110,7 @@ Annuler Changez Choisissez une action + Choisir le jeton Réclamer Réclamez des récompenses Fermer diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3c9bb51610..786a7ceaf2 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -112,6 +112,7 @@ キャンセル 変更 アクションを選択 + トークンを選択 請求 報酬を受け取る 閉じる diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2992c78399..a4bb46b82e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -112,6 +112,7 @@ Отмена Изменить Выберите действие + Выберите токен Получить Вывести награду Закрыть diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index b78180a0ac..e260b556af 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -112,6 +112,7 @@ Скасувати Змінити Виберіть дію + Оберіть токен Отримати Отримати винагороди Закрити diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index e1f37d9840..d5ccd296e0 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -50,6 +50,7 @@ 購買 您尚未授予相機訪問權限,請更改您的隱私設置 删除 + 選擇代幣 關閉 繼續 複製 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 232815179b..f3f8181cf9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -115,6 +115,7 @@ Cancel Change Choose action + Choose token Claim Claim rewards Close @@ -871,6 +872,8 @@ Invalid amount Fee exceeds balance Total amount exceeds balance + Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. + Send with swap Transaction sent Prepare to scan card or ring you want to set up. Forget wallet diff --git a/domain/manage-tokens/models/build.gradle.kts b/domain/manage-tokens/models/build.gradle.kts index 95fb1657d6..d30b3f0186 100644 --- a/domain/manage-tokens/models/build.gradle.kts +++ b/domain/manage-tokens/models/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index 483a320ae9..5cb6e2e818 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -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, diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index 8bbbd4e02b..c3a7b45a50 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -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) diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt new file mode 100644 index 0000000000..0f926c91f3 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index e281e28864..c1d317c3d8 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -4,4 +4,5 @@ enum class ManageTokensSource(val analyticsName: String) { STORIES(analyticsName = "Stories"), ONBOARDING(analyticsName = "Onboarding"), SETTINGS(analyticsName = "Settings"), + SEND_VIA_SWAP(analyticsName = "SendViaSwap"), } \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 1d92f0d67a..87ecb9bb7b 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -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) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt new file mode 100644 index 0000000000..c42147407a --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/di/ChooseManagedTokensModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/di/ChooseManagedTokensModule.kt new file mode 100644 index 0000000000..ff98776324 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/di/ChooseManagedTokensModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt new file mode 100644 index 0000000000..cd7d30b001 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt @@ -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() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManagedTokenUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManagedTokenUM.kt new file mode 100644 index 0000000000..51b2d222b8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManagedTokenUM.kt @@ -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, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt new file mode 100644 index 0000000000..65284e752d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -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 = SlotNavigation() + val uiState: StateFlow + field = MutableStateFlow(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) { + 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 + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensNotificationUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensNotificationUM.kt new file mode 100644 index 0000000000..597c9cb888 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensNotificationUM.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/ui/ChooseManagedTokenContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/ui/ChooseManagedTokenContent.kt new file mode 100644 index 0000000000..632f901ca6 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/ui/ChooseManagedTokenContent.kt @@ -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, + 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) { + 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 \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 65be95e384..732be9f2b7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -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 = MutableStateFlow(getInitialState(params.userWalletId)) val bottomSheetNavigation: SlotNavigation = SlotNavigation() diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 485566a94a..d6b6ca0899 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -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 = MutableStateFlow(getInitialState()) val returnToParentComponentFlow = MutableSharedFlow() diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index e35c8871c7..606d9ae826 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -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 = { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensTopBar.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensTopBar.kt new file mode 100644 index 0000000000..2b7d67794c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensTopBar.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index bf76e4e809..7476e29b4c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -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 = 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>.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 + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index ec165926b1..1be1bb6e11 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -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) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 3e4585d5bf..8c038934c5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -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, private val sourceProvider: Provider, 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>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { - return data - .indexOfFirst { it.id == id } - .takeIf { it != -1 } - ?: error("Currency with currency '$id' not found in batch #$key") - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUtils.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUtils.kt new file mode 100644 index 0000000000..8ea446461a --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUtils.kt @@ -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 { + return List(size = 10) { index -> + CurrencyItemUM.Loading(index) + }.toPersistentList() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt index d37da299a6..c16e25cd84 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt @@ -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) }, ) } \ No newline at end of file