From 0c5d018677198aca94e2df901169a6bb4c1d71d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 19:56:51 +0500 Subject: [PATCH 01/48] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 22 ++++++++++- .../components/notifications/Notification.kt | 4 +- .../notifications/NotificationConfig.kt | 3 ++ .../res/drawable/ic_hardware_backup_36.xml | 22 +++++++++++ .../data/wallets/DefaultWalletsRepository.kt | 14 +++++++ .../wallets/repository/WalletsRepository.kt | 4 ++ ...DismissUpgradeWalletNotificationUseCase.kt | 12 ++++++ ...UpgradeWalletNotificationEnabledUseCase.kt | 13 +++++++ .../preview/PreviewWalletSettingsComponent.kt | 3 ++ .../entity/WalletSettingsItemUM.kt | 8 ++++ .../walletsettings/entity/WalletSettingsUM.kt | 1 + .../model/WalletSettingsModel.kt | 23 +++++++++++- .../walletsettings/ui/WalletSettingsScreen.kt | 37 +++++++++++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 33 +++++++++++++++++ .../wallet/state/model/WalletNotification.kt | 2 + .../components/common/WalletNotifications.kt | 6 --- 16 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_hardware_backup_36.xml create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 44dd702fc3..911eededb2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -25,7 +25,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") @Module @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @@ -352,4 +352,24 @@ internal object WalletsDomainModule { walletsRepository = walletsRepository, ) } + + @Provides + @Singleton + fun providesIsUpgradeWalletNotificationEnabledUseCase( + walletsRepository: WalletsRepository, + ): IsUpgradeWalletNotificationEnabledUseCase { + return IsUpgradeWalletNotificationEnabledUseCase( + walletsRepository = walletsRepository, + ) + } + + @Provides + @Singleton + fun providesDismissUpgradeWalletNotificationUseCase( + walletsRepository: WalletsRepository, + ): DismissUpgradeWalletNotificationUseCase { + return DismissUpgradeWalletNotificationUseCase( + walletsRepository = walletsRepository, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 7b1baa0109..06a2bc1d9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize @@ -64,7 +63,6 @@ fun Notification( NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention }, - iconSize: Dp = 20.dp, isEnabled: Boolean = true, ) { NotificationBaseContainer( @@ -78,7 +76,7 @@ fun Notification( MainContent( iconResId = config.iconResId, iconTint = iconTint, - iconSize = iconSize, + iconSize = config.iconSize, title = config.title, titleColor = titleColor, subtitle = config.subtitle, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 4afe96144e..ca5cdc8e5a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -1,6 +1,8 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.TextReference /** @@ -26,6 +28,7 @@ data class NotificationConfig( val onCloseClick: (() -> Unit)? = null, val showArrowIcon: Boolean = onClick != null, val iconTint: IconTint = IconTint.Unspecified, + val iconSize: Dp = 20.dp, ) { sealed class ButtonsState { diff --git a/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml new file mode 100644 index 0000000000..071683ab08 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 2127d9b80b..d2a4f5cea1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -31,6 +31,7 @@ import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.collections.mutableSetOf typealias SeedPhraseNotificationsStatuses = Map @@ -44,6 +45,9 @@ internal class DefaultWalletsRepository( private val authProvider: AuthProvider, ) : WalletsRepository { + private val upgradeWalletNotificationDisabled: MutableStateFlow> = + MutableStateFlow(mutableSetOf()) + override suspend fun shouldSaveUserWalletsSync(): Boolean { return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } @@ -327,6 +331,16 @@ internal class DefaultWalletsRepository( } } + override fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow { + return upgradeWalletNotificationDisabled.map { + it.contains(userWalletId) + } + } + + override suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) { + upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } + } + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { tangemTechApi.updateWallet( walletId = walletId, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index a1eb1f0cd8..b65e7006cc 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -57,6 +57,10 @@ interface WalletsRepository { suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) + fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow + + suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) + @Throws suspend fun setWalletName(walletId: String, walletName: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt new file mode 100644 index 0000000000..2aeb755fba --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class DismissUpgradeWalletNotificationUseCase( + private val walletsRepository: WalletsRepository, +) { + suspend operator fun invoke(userWalletId: UserWalletId) { + walletsRepository.dismissUpgradeWalletNotification(userWalletId) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt new file mode 100644 index 0000000000..d38fec4fe2 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.flow.Flow + +class IsUpgradeWalletNotificationEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return walletsRepository.isUpgradeWalletNotificationEnabled(userWalletId) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 453120249f..1ac9106632 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -45,6 +45,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onNotificationsDescriptionClick = {}, isNotificationsPermissionGranted = false, onAccessCodeClick = {}, + walletUpgradeDismissed = false, + onUpgradeWalletClick = {}, + onDismissUpgradeWalletClick = {}, ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 490fb2b00e..91a77aec05 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -43,4 +43,12 @@ internal sealed class WalletSettingsItemUM { val title: TextReference, val description: TextReference, ) : WalletSettingsItemUM() + + data class UpgradeWallet( + override val id: String, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + val onDismissClick: () -> Unit, + ) : WalletSettingsItemUM() } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 39bd16ae56..04014e4662 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -10,4 +10,5 @@ internal data class WalletSettingsUM( val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, val isWalletBackedUp: Boolean = true, + val walletUpgradeDismissed: Boolean = false, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index f9d0b633b2..c190745304 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -56,7 +56,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class WalletSettingsModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, @@ -80,6 +80,8 @@ internal class WalletSettingsModel @Inject constructor( private val permissionsRepository: PermissionRepository, private val notificationsRepository: NotificationsRepository, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, + private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, + private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -93,6 +95,7 @@ internal class WalletSettingsModel @Inject constructor( requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, isWalletBackedUp = true, + walletUpgradeDismissed = false, ), ) @@ -120,7 +123,8 @@ internal class WalletSettingsModel @Inject constructor( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), getWalletNFTEnabledUseCase.invoke(params.userWalletId), getWalletNotificationsEnabledUseCase(params.userWalletId), - ) { maybeWallet, nftEnabled, notificationsEnabled -> + isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), + ) { maybeWallet, nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() val isWalletBackedUp = when (wallet) { @@ -139,6 +143,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), + isUpgradeNotificationEnabled = isUpgradeNotificationEnabled, ), isWalletBackedUp = isWalletBackedUp, ) @@ -165,6 +170,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, isNotificationsPermissionGranted: Boolean, + isUpgradeNotificationEnabled: Boolean, ): PersistentList { val isMultiCurrency = when (userWallet) { is UserWallet.Cold -> userWallet.isMultiCurrency @@ -215,6 +221,9 @@ internal class WalletSettingsModel @Inject constructor( onCheckedNotificationsChanged = ::onCheckedNotificationsChange, onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, onAccessCodeClick = ::onAccessCodeClick, + walletUpgradeDismissed = isUpgradeNotificationEnabled, + onUpgradeWalletClick = ::onUpgradeWalletClick, + onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, ) } @@ -367,4 +376,14 @@ internal class WalletSettingsModel @Inject constructor( router.push(AppRoute.UpdateAccessCode(params.userWalletId)) } } + + private fun onUpgradeWalletClick() { + // TODO [REDACTED_TASK_KEY] + } + + private fun onDismissUpgradeWalletClick() { + modelScope.launch { + dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId) + } + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index fe95f8129a..8c4bbd6cf2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -116,6 +117,10 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) + is WalletSettingsItemUM.UpgradeWallet -> UpgradeWalletBlock( + modifier = itemModifier, + model = item, + ) } } } @@ -225,6 +230,21 @@ private fun SwitchBlock(model: WalletSettingsItemUM.WithSwitch, modifier: Modifi } } +@Composable +private fun UpgradeWalletBlock(model: WalletSettingsItemUM.UpgradeWallet, modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + title = model.title, + subtitle = model.description, + iconResId = R.drawable.ic_hardware_backup_36, + iconSize = 36.dp, + onClick = model.onClick, + onCloseClick = model.onDismissClick, + ), + modifier = modifier, + ) +} + @Composable private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermission, modifier: Modifier = Modifier) { Notification( @@ -266,4 +286,21 @@ private fun Preview_WalletSettingsScreen() { PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) } } + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WalletSettingsScreen1() { + TangemThemePreview { + UpgradeWalletBlock( + model = WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = stringReference("Upgrade wallet with a hardware backup"), + description = stringReference("Keep your crypto safe with Tangem’s best-in-class hardware wallet."), + onClick = {}, + onDismissClick = {}, + ), + ) + } +} // endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 5086f22579..4189d78f2a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -47,8 +47,19 @@ internal class ItemsBuilder @Inject constructor( onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, onAccessCodeClick: () -> Unit, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, ): PersistentList = persistentListOf() .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) + .addAll( + buildUpgradeWalletItem( + userWallet = userWallet, + walletUpgradeDismissed = walletUpgradeDismissed, + onUpgradeWalletClick = onUpgradeWalletClick, + onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, + ), + ) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( @@ -124,6 +135,28 @@ internal class ItemsBuilder @Inject constructor( onCheckedChange = onCheckedNFTChange, ) + private fun buildUpgradeWalletItem( + userWallet: UserWallet, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, + ): List = when (userWallet) { + is UserWallet.Cold -> emptyList() + is UserWallet.Hot -> if (!walletUpgradeDismissed) { + listOf( + WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), + description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), + onClick = onUpgradeWalletClick, + onDismissClick = onDismissUpgradeWalletClick, + ), + ) + } else { + emptyList() + } + } + private fun buildNotificationsPermissionItem() = WalletSettingsItemUM.NotificationPermission( id = "notifications_permission", title = resourceReference(id = R.string.transaction_notifications_warning_title), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 965b55df30..3d23c93ae5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference @@ -282,6 +283,7 @@ sealed class WalletNotification(val config: NotificationConfig) { text = resourceReference(R.string.notification_referral_promo_button), onClick = onClick, ), + iconSize = 54.dp, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 0227a5339a..211ce57e2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme @@ -39,11 +38,6 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning is WalletNotification.Informational -> TangemTheme.colors.icon.accent From af7caffba5c63f83648f2a0469fc80f10274ddae Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 09:07:42 +0000 Subject: [PATCH 02/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 418aa40d43..f430a4dd17 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.28-1206" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 35fd75485cf1741a39249ce80832d7fb3801c6ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 16:17:51 +0700 Subject: [PATCH 03/48] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 9 + core/res/src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values-uk-rUA/strings.xml | 10 + core/res/src/main/res/values/strings.xml | 18 ++ .../archived/ArchivedAccountListModel.kt | 10 +- .../createedit/entity/AccountCreateEditUM.kt | 2 +- .../details/entity/AccountDetailsUM.kt | 2 +- .../wallet-settings/impl/build.gradle.kts | 1 + .../impl/DefaultWalletSettingsComponent.kt | 15 ++ .../preview/PreviewWalletSettingsComponent.kt | 37 ++++ .../entity/WalletSettingsItemUM.kt | 33 ++++ .../model/WalletSettingsModel.kt | 11 +- .../walletsettings/ui/WalletSettingsScreen.kt | 171 ++++++++++++++++-- .../utils/AccountItemsDelegate.kt | 111 ++++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 3 + 15 files changed, 413 insertions(+), 30 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2e061c0bce..43ea6c53bf 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,7 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アーカイブされたアカウント 回復する アーカイブ済み アカウントをアーカイブする @@ -26,6 +27,7 @@ 新しいアカウント アカウントを追加 アカウントを編集 + アカウントを長押しして並べ替える 編集を続ける 破棄 新しいアカウントを破棄してもよろしいですか? @@ -116,6 +118,12 @@ 30秒後に再試行するか、カードまたはリングをスキャンしてください 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 + プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。 + プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。 + プロモーションコードが有効になりました + このプロモーションコードは既に使用されているため、再度使うことはできません。 + このプロモーションコードは無効であり、有効化できません。 + ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。 バックアップ処理を開始する 銀行カードまたはその他の支払い方法を使用する @@ -149,6 +157,7 @@ ADAが不足しています。 受け入れる アクセスが拒否されました + アカウント 追加 ポートフォリオに追加 トークンを追加 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index befe0fd2d7..899b661b01 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -59,6 +59,16 @@ Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. + Ошибка активации + Ваш промокод успешно активирован. Бонус 10 USDT в Bitcoin будет зачислен через 14 дней. + Промокод активирован + Этот промокод уже был использован и не может быть активирован повторно. + Код недоступен + Этот промокод недействителен и не может быть активирован. + Неверный код + Для зачисления бонуса нужен Bitcoin-адрес. Добавьте его в портфель и повторите активацию. + Требуется Bitcoin-адрес Начать резервное копирование Используйте банковскую карту или другие методы оплаты 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 5c7d9f4ef4..21880f895f 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -59,6 +59,16 @@ Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше. + Помилка активації + Ваш промокод успішно активовано. Бонус 10 USDT у Bitcoin буде зараховано через 14 днів. + Промокод активовано + Цей промокод уже був використаний і не може бути активований повторно. + Код недоступний + Цей промокод недійсний і не може бути активований. + Невірний код + Для зарахування бонусу потрібна Bitcoin-адреса. Додайте її до портфеля та повторіть активацію. + Потрібна Bitcoin-адреса Почніть процес резервного копіювання Використовуйте банківську картку або інші способи оплати diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dc9ced8f5c..0db4cc4be9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,7 +12,12 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + You cannot create more than %1$s accounts. Archive one to add new. + Can’t add new account + Archived accounts Recover + You’re about to recover “%1$s”. + Recover account Archived Archive account Archive @@ -26,6 +31,8 @@ New account Add account Edit account + %1$s in %2$s + Long tap on an account to reorder accounts Keep Editing Discard Are you sure you want to discard new account? @@ -117,6 +124,12 @@ Please try again in 30 seconds or scan the card or ring Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + An error occurred while processing your promo code. Please try again later. + Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days. + Promo code activated + This promo code has already been used and cannot be activated again. + This promo code is not valid and cannot be activated. + A Bitcoin address is required to receive the bonus. Please add one to your wallet and retry the activation. Start backup process Use a bank card or other payment methods @@ -152,6 +165,7 @@ Not enough ADA Accept Access denied + Accounts Add Add to portfolio Add token @@ -239,6 +253,10 @@ month Network fee Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level + + %d network + %d networks + Next NFT No diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index a93cdcca2b..95554e161b 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -5,9 +5,8 @@ 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.res.R -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.domain.account.usecase.RecoverCryptoPortfolioUseCase @@ -48,8 +47,11 @@ internal class ArchivedAccountListModel @Inject constructor( ) messageSender.send( DialogMessage( - title = stringReference(account.accountName.value), - message = TextReference.EMPTY, + title = resourceReference(R.string.account_archived_recover_dialog_title), + message = resourceReference( + id = R.string.account_archived_recover_dialog_description, + formatArgs = wrappedList(account.accountName.value), + ), firstActionBuilder = { firstAction }, secondActionBuilder = { secondAction }, ), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 330fc2352f..4b4365ba95 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList -data class AccountCreateEditUM( +internal data class AccountCreateEditUM( val title: TextReference, val account: Account, val colorsState: Colors, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt index c68a1f098a..799c295838 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.account.details.entity import com.tangem.common.ui.account.CryptoPortfolioIconUM -data class AccountDetailsUM( +internal data class AccountDetailsUM( val accountName: String, val accountIcon: CryptoPortfolioIconUM, val onCloseClick: () -> Unit, diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index b69105d2c8..3f34abd5a1 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) + implementation(projects.common.ui) /* Project - Domain */ implementation(projects.domain.legacy) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt index 49f23d0f8c..1cb8c2c145 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.component.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -20,6 +22,7 @@ import com.tangem.feature.walletsettings.entity.DialogConfig import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotificationBSConfig import com.tangem.feature.walletsettings.model.WalletSettingsModel import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -61,6 +64,18 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( dialog = { dialog.child?.instance?.Dialog() }, ) + val requestPushPermission = requestPermission( + onAllow = { state.onPushNotificationPermissionGranted(true) }, + onDeny = { state.onPushNotificationPermissionGranted(false) }, + permission = PUSH_PERMISSION, + ) + + if (state.requestPushNotificationsPermission) { + LaunchedEffect(Unit) { + requestPushPermission() + } + } + bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 1ac9106632..69bf4cfb75 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -2,12 +2,19 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.hot.sdk.model.HotWalletId @@ -48,11 +55,41 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { walletUpgradeDismissed = false, onUpgradeWalletClick = {}, onDismissUpgradeWalletClick = {}, + accountsUM = previewAccounts(), ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, ) + private fun previewAccounts() = buildList { + WalletSettingsAccountsUM.Header( + id = "accounts_header", + text = resourceReference(R.string.common_accounts), + ).let(::add) + WalletSettingsAccountsUM.Account( + id = "accountId", + accountName = stringReference("Main account"), + accountIconUM = AccountIconPreviewData.randomAccountIcon(), + tokensInfo = stringReference("10 tokens"), + networksInfo = stringReference("2 networks"), + onClick = {}, + ).let(::add) + WalletSettingsAccountsUM.Footer( + id = "accounts_footer", + addAccount = AddAccountUM( + title = resourceReference(R.string.account_form_title_create), + addAccountEnabled = true, + onAddAccountClick = {}, + ), + archivedAccounts = BlockUM( + text = resourceReference(R.string.account_archived_accounts), + iconRes = R.drawable.ic_archive_24, + onClick = {}, + ), + description = resourceReference(R.string.account_reorder_description), + ).let(::add) + } + @Composable override fun Content(modifier: Modifier) { WalletSettingsScreen( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 91a77aec05..85a6bcb784 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.entity import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -51,4 +52,36 @@ internal sealed class WalletSettingsItemUM { val onClick: () -> Unit, val onDismissClick: () -> Unit, ) : WalletSettingsItemUM() +} + +@Immutable +internal sealed class WalletSettingsAccountsUM : WalletSettingsItemUM() { + + data class Header( + override val id: String, + val text: TextReference, + ) : WalletSettingsAccountsUM() + + data class Account( + override val id: String, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val networksInfo: TextReference, + val onClick: () -> Unit, + ) : WalletSettingsAccountsUM() + + data class Footer( + override val id: String, + val addAccount: AddAccountUM, + val archivedAccounts: BlockUM, + val description: TextReference, + ) : WalletSettingsAccountsUM() { + + data class AddAccountUM( + val title: TextReference, + val addAccountEnabled: Boolean, + val onAddAccountClick: () -> Unit, + ) + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index c190745304..1a5e3ee067 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,11 +15,7 @@ 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.navigation.settings.SettingsManager -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction @@ -29,6 +25,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.DisableWalletNFTUseCase import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase @@ -36,7 +33,6 @@ import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUse import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.repositories.PermissionRepository -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.* import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents @@ -46,6 +42,7 @@ import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotification import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -65,6 +62,7 @@ internal class WalletSettingsModel @Inject constructor( private val messageSender: UiMessageSender, private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, + private val accountItemsDelegate: AccountItemsDelegate, override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, @@ -224,6 +222,7 @@ internal class WalletSettingsModel @Inject constructor( walletUpgradeDismissed = isUpgradeNotificationEnabled, onUpgradeWalletClick = ::onUpgradeWalletClick, onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, + accountsUM = with(accountItemsDelegate) { listOf() }, // todo account ) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 8c4bbd6cf2..b18c80f1f0 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -2,20 +2,29 @@ package com.tangem.feature.walletsettings.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.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountRow +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -30,12 +39,11 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.WalletSettingsScreenTestTags -import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @Composable internal fun WalletSettingsScreen( @@ -71,7 +79,6 @@ internal fun WalletSettingsScreen( private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { LazyColumn( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16, @@ -89,8 +96,17 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { items = state.items, key = WalletSettingsItemUM::id, ) { item -> - val itemModifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) + val offsetModifier = when (item) { + is WalletSettingsAccountsUM.Account, + is WalletSettingsAccountsUM.Footer, + -> Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + else -> Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + } + val itemModifier = offsetModifier .fillMaxWidth() .testTag(WalletSettingsScreenTestTags.SCREEN_ITEM) @@ -121,21 +137,12 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) + is WalletSettingsAccountsUM.Header -> AccountsHeader(item, itemModifier) + is WalletSettingsAccountsUM.Account -> AccountItem(item, itemModifier) + is WalletSettingsAccountsUM.Footer -> AccountsFooter(item, itemModifier) } } } - - val requestPushPermission = requestPermission( - onAllow = { state.onPushNotificationPermissionGranted(true) }, - onDeny = { state.onPushNotificationPermissionGranted(false) }, - permission = PUSH_PERMISSION, - ) - - if (state.requestPushNotificationsPermission) { - LaunchedEffect(Unit) { - requestPushPermission() - } - } } @Composable @@ -257,6 +264,134 @@ private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermi ) } +@Composable +private fun AccountsHeader(model: WalletSettingsAccountsUM.Header, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .background( + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ) + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, + ), + text = model.text.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun AccountItem(model: WalletSettingsAccountsUM.Account, modifier: Modifier = Modifier) { + val subtitle = stringResourceSafe( + id = R.string.account_label_tokens_info, + formatArgs = arrayOf( + model.tokensInfo.resolveReference(), + model.networksInfo.resolveReference(), + ), + ) + AccountRow( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = model.onClick) + .padding(12.dp), + title = model.accountName, + subtitle = stringReference(subtitle), + icon = model.accountIconUM, + ) +} + +@Composable +private fun AccountsFooter(model: WalletSettingsAccountsUM.Footer, modifier: Modifier = Modifier) { + Column(modifier) { + Column( + modifier = Modifier.background( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ), + ) { + AddAccountRow(model.addAccount) + SpacerH( + height = TangemTheme.dimens.size0_5, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary), + ) + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = model.archivedAccounts, + ) + } + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = model.description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AddAccountRow(model: WalletSettingsAccountsUM.Footer.AddAccountUM, modifier: Modifier = Modifier) { + val iconTint: Color + val backgroundColor: Color + val textColor: Color + if (model.addAccountEnabled) { + iconTint = TangemTheme.colors.icon.accent + backgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + textColor = TangemTheme.colors.text.accent + } else { + iconTint = TangemTheme.colors.icon.inactive + backgroundColor = TangemTheme.colors.field.primary + textColor = TangemTheme.colors.text.disabled + } + Row( + modifier = modifier + .clickable(onClick = model.onAddAccountClick) + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(backgroundColor) + .clickable(onClick = { model.onAddAccountClick() }), + ) { + Icon( + modifier = Modifier.size(18.dp), + tint = iconTint, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_24), + contentDescription = null, + ) + } + + Text( + text = model.title.resolveReference(), + color = textColor, + style = TangemTheme.typography.subtitle1, + ) + } +} + @Composable private fun DescriptionWithMoreBlock( model: WalletSettingsItemUM.DescriptionWithMore, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt new file mode 100644 index 0000000000..7e9939d2e3 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.walletsettings.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.account.toUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.pluralReference +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.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM +import com.tangem.feature.walletsettings.impl.R +import javax.inject.Inject + +@ModelScoped +internal class AccountItemsDelegate @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, +) { + + fun buildUiList(userWalletId: UserWalletId, accounts: List): List = buildList { + WalletSettingsAccountsUM.Header( + id = "accounts_header", + text = resourceReference(R.string.common_accounts), + ).let(::add) + + addAll(accounts.map(::mapAccount)) + + val addAccountEnabled = true // todo account + WalletSettingsAccountsUM.Footer( + id = "accounts_footer", + addAccount = AddAccountUM( + title = resourceReference(R.string.account_form_title_create), + addAccountEnabled = addAccountEnabled, + onAddAccountClick = { + if (addAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog() + }, + ), + archivedAccounts = BlockUM( + text = resourceReference(R.string.account_archived_accounts), + iconRes = R.drawable.ic_archive_24, + onClick = { openArchivedAccounts(userWalletId) }, + ), + description = resourceReference(R.string.account_reorder_description), + ).let(::add) + } + + private fun mapAccount(account: Account): WalletSettingsAccountsUM = when (account) { + is Account.CryptoPortfolio -> account.mapCryptoPortfolio() + } + + private fun Account.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM { + return WalletSettingsAccountsUM.Account( + id = accountId.value, + accountName = stringReference(accountName.value), + accountIconUM = icon.toUM(), + tokensInfo = pluralReference( + R.plurals.common_tokens_count, + count = tokensCount, + formatArgs = wrappedList(tokensCount), + ), + networksInfo = pluralReference( + R.plurals.common_networks_count, + count = networksCount, + formatArgs = wrappedList(networksCount), + ), + onClick = { openAccountDetails(this) }, + ) + } + + private fun openAccountDetails(account: Account) { + router.push(AppRoute.AccountDetails(account)) + } + + private fun openArchivedAccounts(userWalletId: UserWalletId) { + router.push(AppRoute.ArchivedAccountList(userWalletId)) + } + + private fun openAddAccount(userWalletId: UserWalletId) { + router.push(AppRoute.CreateAccount(userWalletId)) + } + + private fun canNotAddAccountDialog() { + val firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = { }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_add_limit_dialog_title), + message = resourceReference( + id = R.string.account_add_limit_dialog_description, + formatArgs = wrappedList(MAX_ACCOUNT_COUNT.toString()), + ), + firstActionBuilder = { firstAction }, + ), + ) + } + + companion object { + // todo account use domain const? + private const val MAX_ACCOUNT_COUNT = 20 + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 4189d78f2a..d26dc7fd98 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R import com.tangem.hot.sdk.model.HotWalletId @@ -30,6 +31,7 @@ internal class ItemsBuilder @Inject constructor( fun buildItems( userWallet: UserWallet, userWalletName: String, + accountsUM: List, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, @@ -60,6 +62,7 @@ internal class ItemsBuilder @Inject constructor( onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, ), ) + .addAll(accountsUM) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( From 3e018452037eddaf01e0df62c813d5aeb70cb384 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 16:14:54 +0400 Subject: [PATCH 04/48] Updated on 2026-08-14 --- data/account/build.gradle.kts | 16 +- .../account/converter/AccountConvertersExt.kt | 33 ++++ .../account/converter/AccountListConverter.kt | 46 +++++ .../converter/ArchivedAccountConverter.kt | 29 ++++ .../converter/CryptoPortfolioConverter.kt | 61 +++++++ .../converter/CryptoPortfolioIconConverter.kt | 21 +++ .../GetWalletAccountsResponseConverter.kt | 44 +++++ .../SaveWalletAccountsResponseConverter.kt | 33 ++++ .../converter/TokensGroupTypeConverter.kt | 29 ++++ .../converter/TokensSortTypeConverter.kt | 29 ++++ .../account/converter/AccountConverterExt.kt | 85 ++++++++++ .../converter/AccountListConverterTest.kt | 159 ++++++++++++++++++ .../converter/ArchivedAccountConverterTest.kt | 144 ++++++++++++++++ .../converter/CryptoPortfolioConverterTest.kt | 155 +++++++++++++++++ .../CryptoPortfolioIconConverterTest.kt | 66 ++++++++ .../GetWalletAccountsResponseConverterTest.kt | 130 ++++++++++++++ ...SaveWalletAccountsResponseConverterTest.kt | 50 ++++++ .../converter/TokensGroupTypeConverterTest.kt | 85 ++++++++++ .../converter/TokensSortTypeConverterTest.kt | 81 +++++++++ .../currency/UserTokensResponseFactory.kt | 3 +- .../tangem/domain/models/account/AccountId.kt | 33 ++++ 21 files changed, 1330 insertions(+), 2 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 464ba8486e..287f2b190f 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -9,9 +9,14 @@ android { namespace = "com.tangem.data.account" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { // region Project - Core + implementation(projects.core.datasource) api(projects.core.utils) // endregion @@ -21,7 +26,7 @@ dependencies { // endregion // Project - Data - implementation(projects.core.datasource) + implementation(projects.data.common) // endregion // region DI @@ -34,4 +39,13 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.timber) // endregion + + // region Test + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt new file mode 100644 index 0000000000..ab8eef91a8 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +internal fun String.toAccountId(userWalletId: UserWalletId): AccountId { + return AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId).getOrElse { + error("Unable to create AccountId from value: $this. Cause: $it") + } +} + +internal fun String.toAccountName(): AccountName { + return AccountName(value = this).getOrElse { + error("Unable to create AccountName from value: $this. Cause: $it") + } +} + +internal fun WalletAccountDTO.toIcon(): CryptoPortfolioIcon { + return CryptoPortfolioIconConverter.convert( + value = CryptoPortfolioIconConverter.DataModel(icon = icon, color = iconColor), + ) +} + +internal fun Int.toDerivationIndex(): DerivationIndex { + return DerivationIndex(value = this).getOrElse { + error("Unable to create DerivationIndex from value: $this. Cause: $it") + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt new file mode 100644 index 0000000000..d4c993f59f --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -0,0 +1,46 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [GetWalletAccountsResponse] to an [AccountList] and vice versa + * + * @property userWallet the user wallet associated with the account list + * @param cryptoPortfolioConverterFactory factory to create [CryptoPortfolioConverter] instances + * +[REDACTED_AUTHOR] + */ +internal class AccountListConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: GetWalletAccountsResponse): AccountList { + return AccountList( + userWallet = userWallet, + accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(), + totalAccounts = value.wallet.totalAccounts, + sortType = TokensSortTypeConverter.convert(value.wallet.sort), + groupType = TokensGroupTypeConverter.convert(value.wallet.group), + ) + .getOrElse { + error("Failed to convert GetWalletAccountsResponse to AccountList: $it") + } + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): AccountListConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt new file mode 100644 index 0000000000..072c82e3fc --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.converter.Converter + +/** + * Converts a [WalletAccountDTO] to an [ArchivedAccount] + * + * @param userWalletId the ID of the user wallet associated with the account + * +[REDACTED_AUTHOR] + */ +internal class ArchivedAccountConverter( + private val userWalletId: UserWalletId, +) : Converter { + + override fun convert(value: WalletAccountDTO): ArchivedAccount { + return ArchivedAccount( + accountId = value.id.toAccountId(userWalletId = userWalletId), + name = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + tokensCount = value.totalTokens ?: error("Total tokens should not be null"), + networksCount = value.totalNetworks ?: error("Total networks should not be null"), + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt new file mode 100644 index 0000000000..bae26707a6 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.data.account.converter + +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.TwoWayConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [WalletAccountDTO] to an [Account.CryptoPortfolio] and vise versa + * + * @property userWallet the user wallet associated with the account list + * @property responseCryptoCurrenciesFactory factory to create crypto currencies from response tokens + * +[REDACTED_AUTHOR] + */ +internal class CryptoPortfolioConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val userTokensResponseFactory: UserTokensResponseFactory, +) : TwoWayConverter { + + override fun convert(value: WalletAccountDTO): Account.CryptoPortfolio { + val tokens = value.tokens ?: error("Tokens should not be null") + + return Account.CryptoPortfolio( + accountId = value.id.toAccountId(userWallet.walletId), + accountName = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + cryptoCurrencies = if (tokens.isNotEmpty()) { + responseCryptoCurrenciesFactory.createCurrencies( + tokens = tokens, + userWallet = userWallet, + ).toSet() + } else { + emptySet() + }, + ) + } + + override fun convertBack(value: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = value.accountId.value, + name = value.accountName.value, + derivationIndex = value.derivationIndex.value, + icon = value.icon.value.name, + iconColor = value.icon.color.name, + tokens = value.cryptoCurrencies.map(userTokensResponseFactory::createResponseToken), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): CryptoPortfolioConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt new file mode 100644 index 0000000000..33b8b2ab20 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.account.converter + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.utils.converter.Converter + +/** + * Converts a [CryptoPortfolioIconConverter.DataModel] to a [CryptoPortfolioIcon] + * +[REDACTED_AUTHOR] + */ +internal object CryptoPortfolioIconConverter : Converter { + + override fun convert(value: DataModel): CryptoPortfolioIcon { + return CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.valueOf(value.icon), + color = CryptoPortfolioIcon.Color.valueOf(value.color), + ) + } + + data class DataModel(val icon: String, val color: String) +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..4ae6bf8b41 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** +[REDACTED_AUTHOR] + */ +internal class GetWalletAccountsResponseConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + @Assisted val version: Int, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: AccountList): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = version, + group = TokensGroupTypeConverter.convertBack(value.groupType), + sort = TokensSortTypeConverter.convertBack(value.sortType), + totalAccounts = value.totalAccounts, + ), + accounts = value.accounts + .filterIsInstance() + .map(cryptoPortfolioConverter::convertBack), + unassignedTokens = emptyList(), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet, version: Int): GetWalletAccountsResponseConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..25c5307b84 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.utils.converter.Converter + +/** + * Converts an [AccountList] to a [SaveWalletAccountsResponse] + * +[REDACTED_AUTHOR] + */ +internal object SaveWalletAccountsResponseConverter : Converter { + + override fun convert(value: AccountList): SaveWalletAccountsResponse { + return SaveWalletAccountsResponse( + accounts = value.accounts + .filterIsInstance() + .map(::toDTO), + ) + } + + private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = account.accountId.value, + name = account.accountName.value, + derivationIndex = account.derivationIndex.value, + icon = account.icon.value.name, + iconColor = account.icon.color.name, + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt new file mode 100644 index 0000000000..9a9b92af1b --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.GroupType] to a [TokensGroupType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensGroupTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.GroupType): TokensGroupType { + return when (value) { + UserTokensResponse.GroupType.NETWORK -> TokensGroupType.NETWORK + UserTokensResponse.GroupType.NONE, + UserTokensResponse.GroupType.TOKEN, + -> TokensGroupType.NONE + } + } + + override fun convertBack(value: TokensGroupType): UserTokensResponse.GroupType { + return when (value) { + TokensGroupType.NONE -> UserTokensResponse.GroupType.NONE + TokensGroupType.NETWORK -> UserTokensResponse.GroupType.NETWORK + } + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt new file mode 100644 index 0000000000..91b2b2e88a --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.SortType] to a [TokensSortType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensSortTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.SortType): TokensSortType { + return when (value) { + UserTokensResponse.SortType.BALANCE -> TokensSortType.BALANCE + UserTokensResponse.SortType.MANUAL, + UserTokensResponse.SortType.MARKETCAP, + -> TokensSortType.NONE + } + } + + override fun convertBack(value: TokensSortType): UserTokensResponse.SortType { + return when (value) { + TokensSortType.NONE -> UserTokensResponse.SortType.MANUAL + TokensSortType.BALANCE -> UserTokensResponse.SortType.BALANCE + } + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt new file mode 100644 index 0000000000..5670802776 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +internal fun createWalletAccountDTO( + userWalletId: UserWalletId, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): WalletAccountDTO { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + return WalletAccountDTO( + id = accountId ?: mainAccount.accountId.value, + name = accountName ?: mainAccount.accountName.value, + derivationIndex = derivationIndex ?: mainAccount.derivationIndex.value, + icon = icon ?: mainAccount.icon.value.name, + iconColor = iconColor ?: mainAccount.icon.color.name, + tokens = tokens, + ) +} + +internal fun createCryptoPortfolio(userWalletId: UserWalletId): Account.CryptoPortfolio { + return Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) +} + +internal fun createGetWalletAccountsResponse( + userWalletId: UserWalletId, + groupType: UserTokensResponse.GroupType = UserTokensResponse.GroupType.NETWORK, + sortType: UserTokensResponse.SortType = UserTokensResponse.SortType.BALANCE, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = groupType, + sort = sortType, + totalAccounts = 1, + ), + accounts = buildList { + createWalletAccountDTO( + userWalletId = userWalletId, + accountId = accountId, + accountName = accountName, + icon = icon, + iconColor = iconColor, + derivationIndex = derivationIndex, + tokens = tokens, + ) + .let(::add) + }, + unassignedTokens = emptyList(), + ) +} + +internal fun createAccountList( + userWallet: UserWallet, + sortType: TokensSortType = TokensSortType.BALANCE, + groupType: TokensGroupType = TokensGroupType.NETWORK, +): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf(createCryptoPortfolio(userWallet.walletId)), + totalAccounts = 1, + sortType = sortType, + groupType = groupType, + ) + .getOrNull()!! +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt new file mode 100644 index 0000000000..8b32adb5e5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -0,0 +1,159 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountListConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = AccountListConverter(userWallet, cryptoPortfolioConverterFactory) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val dto = createGetWalletAccountsResponse(userWallet.walletId) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convert(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(dto) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { dto -> + val account = model.expected.getOrNull()!!.accounts + .firstOrNull { it.accountId.value == dto.id } as? Account.CryptoPortfolio + + every { cryptoPortfolioConverter.convert(dto) } returns account!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.TOKEN, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MARKETCAP, + groupType = UserTokensResponse.GroupType.NONE, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 1, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ), + expected = Result.failure( + IllegalStateException( + "Failed to convert GetWalletAccountsResponse to AccountList: EmptyAccountsList: " + + "The accounts list cannot be empty", + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: GetWalletAccountsResponse, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt new file mode 100644 index 0000000000..90831c1d3b --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchivedAccountConverterTest { + + private val userWalletId = UserWalletId("011") + private val converter = ArchivedAccountConverter(userWalletId = userWalletId) + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { converter.convert(value = model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = createDTO(), + expected = Result.success(createDomain()), + ), + TestModel( + value = createDTO(accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + TestModel( + value = createDTO(name = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + TestModel( + value = createDTO(icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = createDTO(iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + TestModel( + value = createDTO(derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + TestModel( + value = createDTO(totalTokens = null), + expected = Result.failure( + IllegalStateException("Total tokens should not be null"), + ), + ), + TestModel( + value = createDTO(totalNetworks = null), + expected = Result.failure( + IllegalStateException("Total networks should not be null"), + ), + ), + ) + } + + private fun createDTO( + accountId: String = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027", + name: String = "Test Account", + icon: String = "Letter", + iconColor: String = "Azure", + derivationIndex: Int = 0, + totalTokens: Int? = 1, + totalNetworks: Int? = 1, + ): WalletAccountDTO { + return WalletAccountDTO( + id = accountId, + name = name, + derivationIndex = derivationIndex, + icon = icon, + iconColor = iconColor, + tokens = null, + totalTokens = totalTokens, + totalNetworks = totalNetworks, + ) + } + + private fun createDomain(): ArchivedAccount { + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(0).getOrNull()!!), + name = "Test Account".toAccountName(), + derivationIndex = 0.toDerivationIndex(), + icon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + tokensCount = 1, + networksCount = 1, + ) + } + + data class TestModel( + val value: WalletAccountDTO, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt new file mode 100644 index 0000000000..f046bae96a --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt @@ -0,0 +1,155 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CryptoPortfolioConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk() + private val userTokensResponseFactory: UserTokensResponseFactory = mockk() + private val converter = CryptoPortfolioConverter( + userWallet = userWallet, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + userTokensResponseFactory = userTokensResponseFactory, + ) + + @BeforeEach + fun setupEach() { + clearMocks(responseCryptoCurrenciesFactory, userTokensResponseFactory) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId), + expected = Result.success(createCryptoPortfolio(userWalletId = userWallet.walletId)), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountName = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, tokens = null), + expected = Result.failure( + IllegalStateException("Tokens should not be null"), + ), + ), + ) + } + } + + data class ConvertModel( + val value: WalletAccountDTO, + val expected: Result, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = converter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = createCryptoPortfolio(userWalletId = userWallet.walletId), + expected = createWalletAccountDTO(userWalletId = userWallet.walletId), + ), + ) + } + } + + data class ConvertBackModel( + val value: Account.CryptoPortfolio, + val expected: WalletAccountDTO, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt new file mode 100644 index 0000000000..c6563f9fc8 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.account.converter.CryptoPortfolioIconConverter.DataModel +import com.tangem.domain.models.account.CryptoPortfolioIcon +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CryptoPortfolioIconConverterTest { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { CryptoPortfolioIconConverter.convert(model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = DataModel(icon = "Letter", color = "Azure"), + expected = Result.success( + CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + ), + TestModel( + value = DataModel(icon = "INVALID_ICON", color = "Azure"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = DataModel(icon = "Letter", color = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ) + } + + data class TestModel( + val value: DataModel, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..b6dddb7aa5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt @@ -0,0 +1,130 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetWalletAccountsResponseConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = GetWalletAccountsResponseConverter( + userWallet = userWallet, + version = 0, + cryptoPortfolioConverterFactory = cryptoPortfolioConverterFactory, + ) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val domain = createAccountList(userWallet = userWallet) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convertBack(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(domain) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { domain -> + val dto = model.expected.getOrNull()!!.accounts.firstOrNull { it.id == domain.accountId.value } + + every { cryptoPortfolioConverter.convertBack(domain as Account.CryptoPortfolio) } returns dto!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.NONE, + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: AccountList, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..13ad114fe4 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt @@ -0,0 +1,50 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SaveWalletAccountsResponseConverterTest { + + @Test + fun convert() { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns UserWalletId("011") + } + + val accountList = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ) + .getOrNull()!! + + // Act + val actual = SaveWalletAccountsResponseConverter.convert(value = accountList) + + // Assert + val expected = SaveWalletAccountsResponse( + accounts = listOf( + WalletAccountDTO( + id = accountList.mainAccount.accountId.value, + name = accountList.mainAccount.accountName.value, + derivationIndex = accountList.mainAccount.derivationIndex.value, + icon = accountList.mainAccount.icon.value.name, + iconColor = accountList.mainAccount.icon.color.name, + ), + ), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt new file mode 100644 index 0000000000..62ba8b6f30 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensGroupTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensGroupTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = UserTokensResponse.GroupType.NETWORK, + expected = TokensGroupType.NETWORK, + ), + ConvertModel( + value = UserTokensResponse.GroupType.NONE, + expected = TokensGroupType.NONE, + ), + ConvertModel( + value = UserTokensResponse.GroupType.TOKEN, + expected = TokensGroupType.NONE, + ), + ) + } + } + + data class ConvertModel( + val value: UserTokensResponse.GroupType, + val expected: TokensGroupType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensGroupTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = TokensGroupType.NETWORK, + expected = UserTokensResponse.GroupType.NETWORK, + ), + ConvertBackModel( + value = TokensGroupType.NONE, + expected = UserTokensResponse.GroupType.NONE, + ), + ) + } + } + + data class ConvertBackModel( + val value: TokensGroupType, + val expected: UserTokensResponse.GroupType, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt new file mode 100644 index 0000000000..e5476f5c49 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt @@ -0,0 +1,81 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensSortTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensSortTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertModel( + value = UserTokensResponse.SortType.BALANCE, + expected = TokensSortType.BALANCE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MANUAL, + expected = TokensSortType.NONE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MARKETCAP, + expected = TokensSortType.NONE, + ), + ) + } + + data class ConvertModel( + val value: UserTokensResponse.SortType, + val expected: TokensSortType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensSortTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertBackModel( + value = TokensSortType.BALANCE, + expected = UserTokensResponse.SortType.BALANCE, + ), + ConvertBackModel( + value = TokensSortType.NONE, + expected = UserTokensResponse.SortType.MANUAL, + ), + ) + } + + data class ConvertBackModel( + val value: TokensSortType, + val expected: UserTokensResponse.SortType, + ) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 4c2623149b..84b75233ac 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -2,8 +2,9 @@ package com.tangem.data.common.currency import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.models.currency.CryptoCurrency +import javax.inject.Inject -class UserTokensResponseFactory { +class UserTokensResponseFactory @Inject constructor() { fun createUserTokensResponse( currencies: List, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index a9cf874078..3133206dd3 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -1,5 +1,8 @@ package com.tangem.domain.models.account +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure import com.tangem.common.extensions.toByteArray import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.toHexString @@ -18,9 +21,39 @@ data class AccountId private constructor( val userWalletId: UserWalletId, ) { + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "AccountId.Error" + + data object Empty : Error { + override fun toString(): String = "$tag: Account ID cannot be blank" + } + + data object InvalidFormat : Error { + override fun toString(): String = "$tag: Account ID must be a 64-character hexadecimal string" + } + } + companion object { private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") } + private val hexRegex = Regex("^[a-fA-F0-9]{64}$") + + /** + * Creates a unique account identifier for a crypto portfolio + * + * @param userWalletId the identifier of the user wallet + * @param value the unique string value representing the account + * + * @return an [Either] containing the [AccountId] on success, or an [Error] on failure + */ + fun forCryptoPortfolio(userWalletId: UserWalletId, value: String): Either = either { + ensure(value.isNotBlank()) { Error.Empty } + ensure(value.matches(hexRegex)) { Error.InvalidFormat } + + AccountId(value = value, userWalletId = userWalletId) + } /** * Creates a unique account identifier for a crypto portfolio From adfbf8d277b7afb54fc7d5eeaf2adc146741a23c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 10:38:49 +0500 Subject: [PATCH 05/48] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheetWithFooter.kt | 24 +++++++++----- .../v2/feeselector/model/FeeSelectorModel.kt | 3 ++ .../ui/FeeSelectorModalBottomSheet.kt | 31 +++++++++++++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 9964ac424d..3826cc503f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.bottomsheets.modal import android.content.res.Configuration +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -49,7 +50,7 @@ inline fun TangemModalBottomSheetWi noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current @@ -84,7 +85,7 @@ inline fun DefaultModalBottomSheetW noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { var isVisible by remember { mutableStateOf(value = config.isShown) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) @@ -118,7 +119,7 @@ inline fun PreviewModalBottomSheetW skipPartiallyExpanded: Boolean = true, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable BoxScope.(T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { BasicModalBottomSheetWithFooter( config = config, @@ -145,7 +146,7 @@ inline fun BasicModalBottomSheetWit noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, modifier: Modifier = Modifier, ) { val model = config.content as? T ?: return @@ -156,8 +157,13 @@ inline fun BasicModalBottomSheetWit val scrollState = rememberScrollState(initial = initial) val isKeyboardOpen by rememberIsKeyboardVisible() - val buttonHeight = TangemTheme.dimens.spacing80 - val contentBottomPadding = TangemTheme.dimens.spacing80 + val buttonHeight by animateDpAsState( + if (footer != null) { + 80.dp + } else { + 0.dp + }, + ) // Offset calculation for keyboard scroll adjustment: // 1) Button height (footer) // 2) Column content bottom padding @@ -202,7 +208,7 @@ inline fun BasicModalBottomSheetWit Column( modifier = Modifier .verticalScroll(state = scrollState) - .padding(bottom = contentBottomPadding), + .padding(bottom = buttonHeight), ) { content(model) } @@ -218,7 +224,9 @@ inline fun BasicModalBottomSheetWit .height(buttonHeight) .align(Alignment.BottomCenter), ) { - footer(model) + if (footer != null) { + footer(model) + } } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 2784044d44..07e8e30c81 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -127,6 +127,9 @@ internal class FeeSelectorModel @Inject constructor( ) } uiState.update(FeeItemSelectedTransformer(feeItem)) + if (feeItem !is FeeItem.Custom) { + onDoneClick() + } } override fun onCustomFeeValueChange(index: Int, value: String) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index c99b647b80..d9aea80f8c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -82,15 +82,19 @@ internal fun FeeSelectorModalBottomSheet( modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, - footer = { - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - enabled = state.isPrimaryButtonEnabled, - text = stringResourceSafe(R.string.common_done), - onClick = feeSelectorIntents::onDoneClick, - ) + footer = if (state.selectedFeeItem is FeeItem.Custom) { + { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.isPrimaryButtonEnabled, + text = stringResourceSafe(R.string.common_done), + onClick = feeSelectorIntents::onDoneClick, + ) + } + } else { + null }, ) } @@ -426,7 +430,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), - selectedFeeItem = customFeeItem, + selectedFeeItem = FeeItem.Slow( + fee = Fee.Common( + Amount( + value = BigDecimal("0.01"), + blockchain = Blockchain.Ethereum, + ), + ), + ), feeExtraInfo = FeeExtraInfo( isFeeApproximate = true, isFeeConvertibleToFiat = true, From 92b55cadbf7bd49ef0a31292b6085676c56e41ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 12:05:42 +0300 Subject: [PATCH 06/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f430a4dd17..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 1d97ce5203d8b25430a59d4d1e605182054aa713 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 14:35:20 +0500 Subject: [PATCH 07/48] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + .../main/res/drawable/ic_knight_shield_24.xml | 9 + .../res/drawable/ic_mobile_security_24.xml | 12 ++ .../src/main/res/drawable/ic_protect_24.xml | 9 + .../ui/src/main/res/drawable/ic_tangem_64.xml | 18 ++ .../hotwallet/UpgradeWalletComponent.kt | 14 ++ .../DefaultUpgradeWalletComponent.kt | 39 +++++ .../upgradewallet/UpgradeWalletModel.kt | 41 +++++ .../upgradewallet/di/UpgradeWalletModule.kt | 25 +++ .../upgradewallet/entity/UpgradeWalletUM.kt | 7 + .../upgradewallet/ui/UpgradeWalletContent.kt | 161 ++++++++++++++++++ 12 files changed, 351 insertions(+) create mode 100644 core/ui/src/main/res/drawable/ic_knight_shield_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_mobile_security_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_protect_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_tangem_64.xml create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 615b1c3778..9a80508e7b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -17,6 +17,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.UpgradeWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.CreateWalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent @@ -103,6 +104,7 @@ internal class ChildFactory @Inject constructor( private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, + private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val walletActivationComponentFactory: WalletActivationComponent.Factory, private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, @@ -486,6 +488,15 @@ internal class ChildFactory @Inject constructor( componentFactory = createMobileWalletComponentFactory, ) } + is AppRoute.UpgradeWallet -> { + createComponentChild( + context = context, + params = UpgradeWalletComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = upgradeWalletComponentFactory, + ) + } is AppRoute.AddExistingWallet -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d0ffcca403..cfa1070c25 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -311,6 +311,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + @Serializable + data class UpgradeWallet( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/upgrade_wallet/${userWalletId.stringValue}") + @Serializable object AddExistingWallet : AppRoute(path = "/add_existing_wallet") diff --git a/core/ui/src/main/res/drawable/ic_knight_shield_24.xml b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml new file mode 100644 index 0000000000..1a058ff367 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_mobile_security_24.xml b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml new file mode 100644 index 0000000000..bbc69392d5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_protect_24.xml b/core/ui/src/main/res/drawable/ic_protect_24.xml new file mode 100644 index 0000000000..5ad478076f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_protect_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_64.xml b/core/ui/src/main/res/drawable/ic_tangem_64.xml new file mode 100644 index 0000000000..6c39da496c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_64.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt new file mode 100644 index 0000000000..8a9b7f6c14 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface UpgradeWalletComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt new file mode 100644 index 0000000000..2f0e91d1de --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.hotwallet.upgradewallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultUpgradeWalletComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: UpgradeWalletComponent.Params, +) : UpgradeWalletComponent, AppComponentContext by context { + + private val model: UpgradeWalletModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + UpgradeWalletContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : UpgradeWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: UpgradeWalletComponent.Params, + ): DefaultUpgradeWalletComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt new file mode 100644 index 0000000000..bc03e54e43 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.hotwallet.upgradewallet + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class UpgradeWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + internal val uiState: StateFlow + field = MutableStateFlow( + UpgradeWalletUM( + onBackClick = { router.pop() }, + onBuyTangemWalletClick = ::onBuyTangemWalletClick, + onScanDeviceClick = ::onScanDeviceClick, + ), + ) + + private fun onBuyTangemWalletClick() { + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanDeviceClick() { + // TODO [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt new file mode 100644 index 0000000000..3beb13bcab --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.upgradewallet.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.DefaultUpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.UpgradeWalletModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface UpgradeWalletModule { + + @Binds + fun bindUpgradeWalletComponentFactory(impl: DefaultUpgradeWalletComponent.Factory): UpgradeWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(UpgradeWalletModel::class) + fun bindUpgradeWalletModel(model: UpgradeWalletModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt new file mode 100644 index 0000000000..8f700a27b9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt @@ -0,0 +1,7 @@ +package com.tangem.features.hotwallet.upgradewallet.entity + +internal data class UpgradeWalletUM( + val onBackClick: () -> Unit, + val onBuyTangemWalletClick: () -> Unit, + val onScanDeviceClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt new file mode 100644 index 0000000000..2ae6ae5d0c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt @@ -0,0 +1,161 @@ +package com.tangem.features.hotwallet.upgradewallet.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = TextReference.EMPTY, + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Icon( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(R.drawable.ic_tangem_64), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 20.dp, + end = 16.dp, + ), + text = stringResourceSafe(R.string.hw_upgrade_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 32.dp), + title = stringResourceSafe(R.string.hw_upgrade_key_migration_title), + description = stringResourceSafe(R.string.hw_upgrade_key_migration_description), + iconRes = R.drawable.ic_mobile_security_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_funds_access_title), + description = stringResourceSafe(R.string.hw_upgrade_funds_access_description), + iconRes = R.drawable.ic_knight_shield_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_general_security_title), + description = stringResourceSafe(R.string.hw_upgrade_general_security_description), + iconRes = R.drawable.ic_protect_24, + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + SecondaryButton( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.details_buy_wallet), + onClick = state.onBuyTangemWalletClick, + ) + PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.hw_upgrade_scan_device), + onClick = state.onScanDeviceClick, + iconResId = R.drawable.ic_tangem_24, + ) + } + } +} + +@Composable +private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 12.dp), + painter = painterResource(iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewUpgradeWalletContent() { + TangemThemePreview { + UpgradeWalletContent( + state = UpgradeWalletUM( + onBackClick = {}, + onBuyTangemWalletClick = {}, + onScanDeviceClick = {}, + ), + ) + } +} \ No newline at end of file From 220a43c1b9d38be882dd718a998f32cde379c6f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 14:36:55 +0500 Subject: [PATCH 08/48] Updated on 2026-08-14 --- .../components/notifications/Notification.kt | 1 + .../notifications/NotificationConfig.kt | 1 + .../hotwallet/accesscode/ui/AccessCode.kt | 2 +- .../hotwallet/common/ui/OptionBlock.kt | 2 +- .../walletbackup/entity/WalletBackupUM.kt | 5 +- .../walletbackup/model/WalletBackupModel.kt | 10 ++-- .../walletbackup/ui/WalletBackupContent.kt | 19 ++++---- .../common/preview/WalletScreenPreviewData.kt | 11 ++++- .../domain/GetMultiWalletWarningsFactory.kt | 46 ++++++++++++++++--- .../wallet/state/model/WalletNotification.kt | 11 +++-- .../components/common/WalletNotifications.kt | 6 +++ 11 files changed, 86 insertions(+), 28 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 06a2bc1d9e..a3e97923bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -62,6 +62,7 @@ fun Notification( NotificationConfig.IconTint.Unspecified -> null NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention + NotificationConfig.IconTint.Warning -> TangemTheme.colors.icon.warning }, isEnabled: Boolean = true, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index ca5cdc8e5a..bd4d9dacef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -65,5 +65,6 @@ data class NotificationConfig( Unspecified, Accent, Attention, + Warning, } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 3924d978c7..4596985307 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -74,7 +74,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = !state.isConfirmMode, + isPasswordVisual = state.isConfirmMode, value = state.accessCode, pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index 8e90402e09..c41dba03e8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -61,7 +61,7 @@ internal fun OptionBlock( color = backgroundColor, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .conditional(onClick != null) { + .conditional(onClick != null && enabled) { onClick?.let { clickableSingle(onClick = it) } ?: Modifier } .padding(16.dp), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index f3f17cd9d3..3bc262d1a9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -4,8 +4,9 @@ import com.tangem.core.ui.components.label.entity.LabelUM internal data class WalletBackupUM( val onBackClick: () -> Unit, - val recoveryPhraseStatus: LabelUM?, - val googleDriveStatus: LabelUM?, + val recoveryPhraseOption: LabelUM?, + val googleDriveOption: LabelUM?, + val googleDriveStatus: BackupStatus, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, val backedUp: Boolean, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index b398ed047a..9996c7fb7a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -40,14 +41,15 @@ internal class WalletBackupModel @Inject constructor( field = MutableStateFlow( WalletBackupUM( onBackClick = { router.pop() }, - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + googleDriveStatus = BackupStatus.ComingSoon, onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, backedUp = false, @@ -95,7 +97,7 @@ internal class WalletBackupModel @Inject constructor( } private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy( - recoveryPhraseStatus = if (userWallet.backedUp) { + recoveryPhraseOption = if (userWallet.backedUp) { LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, @@ -106,7 +108,7 @@ internal class WalletBackupModel @Inject constructor( style = LabelStyle.WARNING, ) }, - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index f5d25b7280..671c8dd89a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -51,7 +51,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_seed_title), description = stringResourceSafe(R.string.hw_backup_seed_description), badge = { - state.recoveryPhraseStatus?.let { Label(it) } + state.recoveryPhraseOption?.let { Label(it) } }, onClick = state.onRecoveryPhraseClick, enabled = true, @@ -63,7 +63,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_google_drive_title), description = stringResourceSafe(R.string.hw_backup_google_drive_description), badge = { - state.googleDriveStatus?.let { Label(it) } + state.googleDriveOption?.let { Label(it) } }, onClick = state.onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, @@ -85,42 +85,45 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider: private class WalletBackupUMProvider : CollectionPreviewParameterProvider( collection = listOf( WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + googleDriveStatus = BackupStatus.ComingSoon, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), + googleDriveStatus = BackupStatus.NoBackup, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, ), + googleDriveStatus = BackupStatus.Done, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index d57ed3079f..9e485dad2e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -117,7 +120,13 @@ internal object WalletScreenPreviewData { buttons = persistentListOf(buyButton), warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, - WalletNotification.FinishWalletActivation { }, + WalletNotification.FinishWalletActivation( + iconTint = NotificationConfig.IconTint.Attention, + buttonsState = ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = { }, + ), + ), ), bottomSheetConfig = null, tokensListState = textContentTokensState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 248f54a3e6..a7e173397b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce @@ -19,7 +22,9 @@ import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.utils.extensions.isPositive import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -54,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) - addFinishWalletActivationNotification(userWallet, clickIntents) + addFinishWalletActivationNotification(userWallet, maybeTokenList, clickIntents) addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) @@ -256,26 +261,55 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { - if (condition) add(element = element) - } - private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, + maybeTokenList: Lce, clickIntents: WalletClickIntents, ) { if (userWallet !is UserWallet.Hot) return val shouldShowFinishActivation = !userWallet.backedUp + val iconTint = maybeTokenList.fold( + ifLoading = { + if ((it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifContent = { + if ((it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifError = { IconTint.Attention }, + ) + addIf( element = WalletNotification.FinishWalletActivation( - onFinishClick = clickIntents::onFinishWalletActivationClick, + iconTint = iconTint, + buttonsState = when (iconTint) { + IconTint.Warning -> ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + else -> ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + }, ), condition = shouldShowFinishActivation, ) } + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) add(element = element) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 3d23c93ae5..dd6ae1ec2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference @@ -257,16 +259,15 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class FinishWalletActivation( - val onFinishClick: () -> Unit, + val iconTint: IconTint, + val buttonsState: ButtonsState, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), subtitle = resourceReference(R.string.hw_activation_need_description), iconResId = R.drawable.img_knight_shield_32, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.hw_activation_need_finish), - onClick = onFinishClick, - ), + iconTint = iconTint, + buttonsState = buttonsState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 211ce57e2d..172ab84fbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -34,6 +34,12 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + Notification( + config = it.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + ) + } else -> { Notification( config = it.config, From 072290b144183fdd2151e9fc460c6360f7b8bf0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 11:05:10 +0500 Subject: [PATCH 09/48] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 ++ features/yield-lending/api/.gitignore | 1 + features/yield-lending/api/build.gradle.kts | 24 +++++++++++ .../api/YieldLendingFeatureToggles.kt | 6 +++ features/yield-lending/impl/.gitignore | 1 + features/yield-lending/impl/build.gradle.kts | 41 +++++++++++++++++++ .../impl/DefaultYieldLendingFeatureToggles.kt | 11 +++++ .../impl/di/YieldLendingFeatureModule.kt | 21 ++++++++++ settings.gradle.kts | 3 ++ 9 files changed, 112 insertions(+) create mode 100644 features/yield-lending/api/.gitignore create mode 100644 features/yield-lending/api/build.gradle.kts create mode 100644 features/yield-lending/api/src/main/java/com/tangem/features/yieldlending/api/YieldLendingFeatureToggles.kt create mode 100644 features/yield-lending/impl/.gitignore create mode 100644 features/yield-lending/impl/build.gradle.kts create mode 100644 features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/DefaultYieldLendingFeatureToggles.kt create mode 100644 features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/di/YieldLendingFeatureModule.kt diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index bd1f7e6e67..318f39ce65 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -62,5 +62,9 @@ { "name": "NEW_TOKEN_RECEIVE_ENABLED", "version": "5.28.0" + }, + { + "name": "YIELD_LENDING_FEATURE_ENABLED", + "version": "undefined" } ] diff --git a/features/yield-lending/api/.gitignore b/features/yield-lending/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/yield-lending/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/yield-lending/api/build.gradle.kts b/features/yield-lending/api/build.gradle.kts new file mode 100644 index 0000000000..da7d806bb3 --- /dev/null +++ b/features/yield-lending/api/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.yieldlending.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/yield-lending/api/src/main/java/com/tangem/features/yieldlending/api/YieldLendingFeatureToggles.kt b/features/yield-lending/api/src/main/java/com/tangem/features/yieldlending/api/YieldLendingFeatureToggles.kt new file mode 100644 index 0000000000..3d1c8a10bd --- /dev/null +++ b/features/yield-lending/api/src/main/java/com/tangem/features/yieldlending/api/YieldLendingFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.yieldlending.api + +interface YieldLendingFeatureToggles { + + val isYieldLendingFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/yield-lending/impl/.gitignore b/features/yield-lending/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/yield-lending/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/yield-lending/impl/build.gradle.kts b/features/yield-lending/impl/build.gradle.kts new file mode 100644 index 0000000000..40ebde87d3 --- /dev/null +++ b/features/yield-lending/impl/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.yieldlending.impl" +} + +dependencies { + + /** Feature */ + implementation(projects.features.yieldLending.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.androidx.activity.compose) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/DefaultYieldLendingFeatureToggles.kt b/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/DefaultYieldLendingFeatureToggles.kt new file mode 100644 index 0000000000..4e780b4e96 --- /dev/null +++ b/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/DefaultYieldLendingFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.yieldlending.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.yieldlending.api.YieldLendingFeatureToggles + +internal class DefaultYieldLendingFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : YieldLendingFeatureToggles { + override val isYieldLendingFeatureEnabled: Boolean + get() = featureToggles.isFeatureEnabled("YIELD_LENDING_FEATURE_ENABLED") +} \ No newline at end of file diff --git a/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/di/YieldLendingFeatureModule.kt b/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/di/YieldLendingFeatureModule.kt new file mode 100644 index 0000000000..d666cb36a1 --- /dev/null +++ b/features/yield-lending/impl/src/main/java/com/tangem/features/yieldlending/impl/di/YieldLendingFeatureModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.yieldlending.impl.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.yieldlending.api.YieldLendingFeatureToggles +import com.tangem.features.yieldlending.impl.DefaultYieldLendingFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object YieldLendingFeatureModule { + + @Singleton + @Provides + fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldLendingFeatureToggles { + return DefaultYieldLendingFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d785e00c38..7c7251cb46 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -281,6 +281,9 @@ include(":features:account:impl") include(":features:token-recieve:api") include(":features:token-recieve:impl") + +include(":features:yield-lending:api") +include(":features:yield-lending:impl") // endregion Feature modules // region Domain modules From 3a96b203e0171f2375ddc0b31c1c74f30381055f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 19:14:43 +0700 Subject: [PATCH 10/48] Updated on 2026-08-14 --- .../tangem/common/ui/account/AccountRow.kt | 5 ++ .../converter/UserWalletItemUMConverter.kt | 17 +--- features/markets/impl/build.gradle.kts | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 7 +- .../impl/model/MarketsPortfolioModel.kt | 59 ++++++------ .../impl/model/MyPortfolioUMFactory.kt | 6 +- .../wallet-settings/impl/build.gradle.kts | 2 + .../preview/PreviewWalletSettingsComponent.kt | 15 +++- .../entity/WalletSettingsItemUM.kt | 4 +- .../model/WalletSettingsModel.kt | 36 +++----- .../walletsettings/ui/WalletSettingsScreen.kt | 50 +++++++---- .../walletsettings/utils/ItemsBuilder.kt | 18 +--- .../utils/WalletCardItemDelegate.kt | 55 ++++++++++++ features/wallet/api/build.gradle.kts | 3 + .../wallet/utils/UserWalletImageFetcher.kt | 20 +++++ .../feature/wallet/di/WalletFeatureModule.kt | 7 ++ .../utils/DefaultUserWalletImageFetcher.kt | 89 +++++++++++++++++++ .../wallet/utils/DefaultUserWalletsFetcher.kt | 27 +----- 18 files changed, 285 insertions(+), 136 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt create mode 100644 features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt index c48fa40072..d02fe62c15 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference @@ -73,6 +74,8 @@ private fun Title(title: TextReference) { text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } @@ -82,6 +85,8 @@ private fun Subtitle(subtitle: TextReference) { color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, text = subtitle.resolveReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 41f2c3611e..41b0727d2d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter import com.tangem.common.ui.R import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -12,7 +12,6 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet @@ -37,10 +36,10 @@ class UserWalletItemUMConverter( private val isBalanceHidden: Boolean = false, private val authMode: Boolean = false, private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, - private val artwork: ArtworkModel? = null, + artwork: UserWalletItemUM.ImageState? = null, ) : Converter { - private val artworkUMConverter = ArtworkUMConverter() + private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading override fun convert(value: UserWallet): UserWalletItemUM { return with(value) { @@ -52,7 +51,7 @@ class UserWalletItemUMConverter( isEnabled = isEnabled(userWallet = this), endIcon = endIcon, onClick = { onClick(value.walletId) }, - imageState = getImageState(userWallet = value), + imageState = artwork, label = getLabelOrNull(userWallet = this), ) } @@ -73,14 +72,6 @@ class UserWalletItemUMConverter( } } - private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState { - return when { - userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet - artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork)) - else -> UserWalletItemUM.ImageState.Loading - } - } - private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 1b9562b290..61d498c799 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { api(projects.features.onramp.api) api(projects.features.sendV2.api) api(projects.features.tokenRecieve.api) + api(projects.features.wallet.api) /* Data */ implementation(projects.data.common) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index feef86486d..49b7fead68 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -56,7 +55,7 @@ internal class AddToPortfolioBSContentUMFactory( portfolioUIData: PortfolioUIData, selectedWallet: UserWallet?, alreadyAddedNetworks: Set?, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return (currentState ?: TangemBottomSheetConfig.Empty).copy( isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, @@ -110,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory( } private fun UserWallet.toSelectedUserWalletItemUM( - artwork: ArtworkModel? = null, + artwork: UserWalletItemUM.ImageState? = null, portfolioData: PortfolioData, balance: TotalFiatBalance?, ): UserWalletItemUM { @@ -128,7 +127,7 @@ internal class AddToPortfolioBSContentUMFactory( isShow: Boolean, portfolioData: PortfolioData, selectedWalletId: UserWalletId, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( isShown = isShow, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 60673724c0..5b6bd84ca2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -21,7 +22,6 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency @@ -31,7 +31,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.transaction.usecase.GetEnsNameUseCase -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.markets.impl.R @@ -42,13 +41,14 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject @@ -66,21 +66,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val getCardImageUseCase: GetCardImageUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, private val getEnsNameUseCase: GetEnsNameUseCase, + private val userWalletImageFetcher: UserWalletImageFetcher, ) : Model() { val state: StateFlow get() = _state private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - private val loadedArtworks: HashMap = hashMapOf() - private val artworksState: MutableStateFlow> = MutableStateFlow(hashMapOf()) - private val loadArtworksMutex = Mutex() - private val params = paramsContainer.require() private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( token = params.token, @@ -196,38 +192,33 @@ internal class MarketsPortfolioModel @Inject constructor( private fun subscribeOnStateUpdates() { combine( - flow = loadPortfolioData(params.token.id), + flow = loadPortfolioDataWithArtworks(params.token.id), flow2 = getPortfolioUIDataFlow(), - flow3 = artworksState, - transform = factory::create, + transform = { pair, portfolioUIData -> + val (portfolioData, artworks) = pair + factory.create(portfolioData, portfolioUIData, artworks) + }, ) .onEach { _state.value = it } .launchIn(modelScope) } - private fun loadPortfolioData(currencyRawId: CryptoCurrency.RawID): Flow { - portfolioDataLoader.load(currencyRawId).onEach { - loadArtworks(it.walletsWithCurrencies.keys.toList()) - }.also { return it } - } + private fun loadPortfolioDataWithArtworks( + currencyRawId: CryptoCurrency.RawID, + ): Flow>> { + val wallets = Channel>() + val portfolioFlow = portfolioDataLoader + .load(currencyRawId) + .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - private fun loadArtworks(wallets: List) { - modelScope.launch { - loadArtworksMutex.withLock { - wallets.filterIsInstance().forEach { wallet -> - if (!loadedArtworks.containsKey(wallet.walletId)) { - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - ) - loadedArtworks[wallet.walletId] = artwork - artworksState.emit(loadedArtworks) - } - } - } - } + val artworksFlow = wallets.receiveAsFlow() + .distinctUntilChanged() + .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } + + return combine( + flow = portfolioFlow, + flow2 = artworksFlow, + ) { portfolioData, artworks -> portfolioData to artworks } } private fun getPortfolioUIDataFlow(): Flow { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 5c5bb59b96..44fd810a9a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -1,8 +1,8 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -36,7 +36,7 @@ internal class MyPortfolioUMFactory( fun create( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): MyPortfolioUM { val addToPortfolioData = portfolioUIData.addToPortfolioData @@ -89,7 +89,7 @@ internal class MyPortfolioUMFactory( private fun createAddToPortfolioBSConfig( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { val selectedWallet = portfolioData.walletsWithCurrencies.keys .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 3f34abd5a1..4ab6bc0a40 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.features.onboardingV2.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.wallet.api) /* Project - Core */ implementation(projects.core.decompose) @@ -69,4 +70,5 @@ dependencies { /** Tangem libraries */ implementation(tangemDeps.hot.core) + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 69bf4cfb75..e716a35761 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.ui.components.block.model.BlockUM @@ -13,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.ui.WalletSettingsScreen @@ -34,14 +36,11 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { wallets = null, backedUp = false, ), - userWalletName = "My Wallet", isReferralAvailable = true, isLinkMoreCardsAvailable = true, - isRenameWalletAvailable = false, isNFTFeatureEnabled = true, isNFTEnabled = true, onCheckedNFTChange = {}, - renameWallet = {}, forgetWallet = {}, onLinkMoreCardsClick = {}, onReferralClick = {}, @@ -56,6 +55,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onUpgradeWalletClick = {}, onDismissUpgradeWalletClick = {}, accountsUM = previewAccounts(), + cardItem = previewCardBlock(), ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, @@ -90,6 +90,15 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { ).let(::add) } + private fun previewCardBlock() = WalletSettingsItemUM.CardBlock( + id = "wallet_name", + title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), + text = stringReference("Wallet Name"), + isEnabled = true, + onClick = { }, + imageState = ImageState.MobileWallet, + ) + @Composable override fun Content(modifier: Modifier) { WalletSettingsScreen( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 85a6bcb784..d2cb70fe36 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.account.CryptoPortfolioIconUM +import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -24,11 +25,12 @@ internal sealed class WalletSettingsItemUM { val onCheckedChange: (Boolean) -> Unit, ) : WalletSettingsItemUM() - data class WithText( + data class CardBlock( override val id: String, val title: TextReference, val text: TextReference, val isEnabled: Boolean, + val imageState: ImageState, val onClick: () -> Unit, ) : WalletSettingsItemUM() diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 1a5e3ee067..66ddf7f18f 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.model import android.os.Build +import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -44,6 +45,7 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList @@ -66,7 +68,7 @@ internal class WalletSettingsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, - private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + walletCardItemDelegateFactory: WalletCardItemDelegate.Factory, private val isDemoCardUseCase: IsDemoCardUseCase, getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, private val enableWalletNFTUseCase: EnableWalletNFTUseCase, @@ -85,6 +87,7 @@ internal class WalletSettingsModel @Inject constructor( val params: WalletSettingsComponent.Params = paramsContainer.require() val dialogNavigation = SlotNavigation() val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val walletCardItemDelegate = walletCardItemDelegateFactory.create(dialogNavigation) val state: MutableStateFlow = MutableStateFlow( value = WalletSettingsUM( @@ -117,14 +120,12 @@ internal class WalletSettingsModel @Inject constructor( } init { - combine( - getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), + fun combineUI(wallet: UserWallet) = combine( getWalletNFTEnabledUseCase.invoke(params.userWalletId), getWalletNotificationsEnabledUseCase(params.userWalletId), isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), - ) { maybeWallet, nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled -> - val wallet = maybeWallet.getOrNull() ?: return@combine - val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + walletCardItemDelegate.cardItemFlow(wallet), + ) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem -> val isWalletBackedUp = when (wallet) { is UserWallet.Hot -> wallet.backedUp is UserWallet.Cold -> true @@ -135,8 +136,7 @@ internal class WalletSettingsModel @Inject constructor( value.copy( items = buildItems( userWallet = wallet, - dialogNavigation = dialogNavigation, - isRenameWalletAvailable = isRenameWalletAvailable, + cardItem = cardItem, isNFTEnabled = nftEnabled, isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, @@ -147,6 +147,10 @@ internal class WalletSettingsModel @Inject constructor( ) } } + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .filterIsInstance>() + .flatMapLatest { combineUI(it.value) } .launchIn(modelScope) } @@ -162,8 +166,7 @@ internal class WalletSettingsModel @Inject constructor( private fun buildItems( userWallet: UserWallet, - dialogNavigation: SlotNavigation, - isRenameWalletAvailable: Boolean, + cardItem: WalletSettingsItemUM.CardBlock, isNFTEnabled: Boolean, isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, @@ -176,7 +179,7 @@ internal class WalletSettingsModel @Inject constructor( } return itemsBuilder.buildItems( userWallet = userWallet, - userWalletName = userWallet.name, + cardItem = cardItem, isReferralAvailable = when (userWallet) { is UserWallet.Cold -> userWallet.cardTypesResolver.isTangemWallet() is UserWallet.Hot -> false @@ -186,8 +189,6 @@ internal class WalletSettingsModel @Inject constructor( is UserWallet.Hot -> false }, isManageTokensAvailable = isMultiCurrency, - isRenameWalletAvailable = isRenameWalletAvailable, - renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, isNFTFeatureEnabled = isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, @@ -226,15 +227,6 @@ internal class WalletSettingsModel @Inject constructor( ) } - private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { - val config = DialogConfig.RenameWallet( - userWalletId = userWallet.walletId, - currentName = userWallet.name, - ) - - dialogNavigation.activate(config) - } - private fun forgetWallet() = modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { Timber.e("Unable to delete wallet: $it") diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index b18c80f1f0..c0d5e0cd86 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.common.ui.account.AccountRow +import com.tangem.common.ui.userwallet.CardImage import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.TangemSwitch @@ -30,10 +31,13 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockItem +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -115,7 +119,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) - is WalletSettingsItemUM.WithText -> TextBlock( + is WalletSettingsItemUM.CardBlock -> CardBlock( modifier = itemModifier, model = item, ) @@ -180,30 +184,40 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier } @Composable -private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) { +private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier = Modifier) { BlockCard( modifier = modifier.fillMaxWidth(), enabled = model.isEnabled, onClick = model.onClick, ) { - Column( + Row( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = model.title.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - Text( - text = model.text.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - overflow = TextOverflow.Ellipsis, + CardImage(model.imageState) + Column(modifier = Modifier.weight(1f)) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + SecondarySmallButton( + config = SmallButtonConfig( + enabled = model.isEnabled, + text = resourceReference(R.string.common_rename), + onClick = model.onClick, + ), ) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index d26dc7fd98..6d2bf36754 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,7 +9,6 @@ import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -30,12 +29,11 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") fun buildItems( userWallet: UserWallet, - userWalletName: String, + cardItem: WalletSettingsItemUM.CardBlock, accountsUM: List, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, - isRenameWalletAvailable: Boolean, isNFTFeatureEnabled: Boolean, isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit, @@ -45,7 +43,6 @@ internal class ItemsBuilder @Inject constructor( onCheckedNotificationsChanged: (Boolean) -> Unit, onNotificationsDescriptionClick: () -> Unit, forgetWallet: () -> Unit, - renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, onAccessCodeClick: () -> Unit, @@ -53,7 +50,7 @@ internal class ItemsBuilder @Inject constructor( onUpgradeWalletClick: () -> Unit, onDismissUpgradeWalletClick: () -> Unit, ): PersistentList = persistentListOf() - .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) + .add(cardItem) .addAll( buildUpgradeWalletItem( userWallet = userWallet, @@ -62,8 +59,8 @@ internal class ItemsBuilder @Inject constructor( onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, ), ) - .addAll(accountsUM) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) + .addAll(accountsUM) .add( buildCardItem( userWallet = userWallet, @@ -121,15 +118,6 @@ internal class ItemsBuilder @Inject constructor( } } - private fun buildNameItem(walletName: String, isRenameWalletAvailable: Boolean, renameWallet: () -> Unit) = - WalletSettingsItemUM.WithText( - id = "wallet_name", - title = resourceReference(id = R.string.settings_wallet_name_title), - text = stringReference(walletName), - isEnabled = isRenameWalletAvailable, - onClick = renameWallet, - ) - private fun buildNFTItem(isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit) = WalletSettingsItemUM.WithSwitch( id = "nft", diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt new file mode 100644 index 0000000000..322eaaac62 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.walletsettings.utils + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow + +internal class WalletCardItemDelegate @AssistedInject constructor( + private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + private val walletImageFetcher: UserWalletImageFetcher, + @Assisted private val dialogNavigation: SlotNavigation, +) { + + fun cardItemFlow(wallet: UserWallet): Flow = combine( + flow = walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL), + flow2 = flow { emit(getShouldSaveUserWalletsSyncUseCase()) }, + transform = { imageState, isRenameAvailable -> + val walletName = wallet.name + WalletSettingsItemUM.CardBlock( + id = "wallet_name", + title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), + text = stringReference(walletName), + isEnabled = isRenameAvailable, + onClick = { openRenameWalletDialog(wallet) }, + imageState = imageState, + ) + }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + dialogNavigation.activate(config) + } + + @AssistedFactory + interface Factory { + fun create(dialogNavigation: SlotNavigation): WalletCardItemDelegate + } +} \ No newline at end of file diff --git a/features/wallet/api/build.gradle.kts b/features/wallet/api/build.gradle.kts index 3b1f5eeea2..4c23e065f7 100644 --- a/features/wallet/api/build.gradle.kts +++ b/features/wallet/api/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { /** Project - Domain */ implementation(projects.domain.models) + /** Tangem libraries */ + implementation(tangemDeps.card.core) + /** Core */ implementation(projects.core.ui) implementation(projects.core.decompose) diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt new file mode 100644 index 0000000000..42738f4e11 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt @@ -0,0 +1,20 @@ +package com.tangem.features.wallet.utils + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.Flow + +interface UserWalletImageFetcher { + + fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow + fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow + fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow + + fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt index 74a02c34b7..1a925f467e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt @@ -4,8 +4,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.feature.wallet.DefaultWalletEntryComponent import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel import com.tangem.feature.wallet.child.wallet.model.WalletModel +import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher import com.tangem.feature.wallet.utils.DefaultUserWalletsFetcher import com.tangem.features.wallet.WalletEntryComponent +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import dagger.Binds import dagger.Module @@ -13,6 +15,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -24,6 +27,10 @@ internal interface WalletFeatureModule { @Binds fun bindUserWalletsFetcher(impl: DefaultUserWalletsFetcher.Factory): UserWalletsFetcher.Factory + @Binds + @Singleton + fun bindUserWalletImageFetcher(impl: DefaultUserWalletImageFetcher): UserWalletImageFetcher + @Binds @IntoMap @ClassKey(WalletModel::class) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt new file mode 100644 index 0000000000..f6f11319dd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.utils + +import arrow.core.Either +import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class DefaultUserWalletImageFetcher @Inject constructor( + private val getCardImageUseCase: GetCardImageUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val artworkUMConverter: ArtworkUMConverter, +) : UserWalletImageFetcher { + + private val smallCache = MutableStateFlow(mapOf()) + private val largeCache = MutableStateFlow(mapOf()) + + override fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow = when (wallet) { + is UserWallet.Cold -> walletImage(wallet.scanResponse.card, size) + is UserWallet.Hot -> flowOf(UserWalletItemUM.ImageState.MobileWallet) + } + + override fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> = wallets + .map { userWallet -> walletImage(userWallet, size).map { imageState -> userWallet.walletId to imageState } } + .merge() + .runningFold(mapOf()) { map, newState -> map.plus(newState) } + .filter { it.size >= wallets.size } // prevent spam, waiting full map + .distinctUntilChanged() + + override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow = flow { + val imagesFlow = getUserWalletUseCase.invokeFlow(walletId) + // emit Loading and wait wallet + .onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) } + .filterIsInstance>() + .map { it.value } + .distinctUntilChanged() + .flatMapLatest { wallet -> walletImage(wallet, size) } + emitAll(imagesFlow) + }.distinctUntilChanged() + + override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow = + internalGetCardImage( + cardInfo = cardDTO, + size = size, + ).distinctUntilChanged() + + private fun internalGetCardImage(cardInfo: CardDTO, size: ArtworkSize): Flow = flow { + emit(cacheOrLoading(cardInfo.cardId, size)) + + val artwork = getCardImageUseCase.invoke( + cardId = cardInfo.cardId, + cardPublicKey = cardInfo.cardPublicKey, + size = size, + manufacturerName = cardInfo.manufacturer.name, + firmwareVersion = cardInfo.firmwareVersion.toSdkFirmwareVersion(), + ) + .let { artworkUMConverter.convert(it) } + .also { save(cardInfo.cardId, size, it) } + emit(UserWalletItemUM.ImageState.Image(artwork)) + } + + private fun cacheOrLoading(cardId: String, size: ArtworkSize): UserWalletItemUM.ImageState { + val artwork = when (size) { + ArtworkSize.LARGE -> largeCache.value[cardId] + ArtworkSize.SMALL -> smallCache.value[cardId] + } + return artwork + ?.let { UserWalletItemUM.ImageState.Image(artwork) } + ?: UserWalletItemUM.ImageState.Loading + } + + private fun save(cardId: String, size: ArtworkSize, artwork: ArtworkUM) { + when (size) { + ArtworkSize.LARGE -> largeCache.update { it.plus(cardId to artwork) } + ArtworkSize.SMALL -> smallCache.update { it.plus(cardId to artwork) } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index 7a3b9accba..6a310e5c31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -14,16 +14,15 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.toLce -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -45,11 +44,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @Assisted private val messageSender: UiMessageSender, @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, @Assisted("authMode") private val authMode: Boolean, - private val getCardImageUseCase: GetCardImageUseCase, + private val userWalletImageFetcher: UserWalletImageFetcher, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { - private var loadedArtworks: HashMap = hashMapOf() private val walletsFlow = if (onlyMultiCurrency) getWalletsUseCase().map { it.filter { it.isMultiCurrency } } else getWalletsUseCase() @@ -67,7 +65,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), - flow4 = loadArtworks(wallets), + flow4 = userWalletImageFetcher.walletsImage(wallets, ArtworkSize.SMALL), ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> createUiModels( wallets = wallets, @@ -90,29 +88,12 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( } .flowOn(dispatchers.default) - private fun loadArtworks(wallets: List): Flow> { - return flow { - emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish - wallets.filterIsInstance().forEach { wallet -> - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - size = ArtworkSize.SMALL, - ) - loadedArtworks[wallet.walletId] = artwork - emit(loadedArtworks) - } - } - } - private fun createUiModels( wallets: List, maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - artworks: HashMap, + artworks: Map, ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, From 710002e992ce3e3026e83315d8bbaa5035193a8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 19:31:47 +0400 Subject: [PATCH 11/48] Updated on 2026-08-14 --- data/account/build.gradle.kts | 10 +- .../AccountConverterFactoryContainer.kt | 20 ++++ .../store/AccountsResponseStoreFactory.kt | 66 ++++++++++++++ .../store/AccountsResponseStoreFactoryTest.kt | 91 +++++++++++++++++++ .../createedit/AccountCreateEditModel.kt | 18 +++- 5 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 287f2b190f..90800c1228 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -25,18 +25,24 @@ dependencies { api(projects.domain.models) // endregion - // Project - Data + // region Project - Data implementation(projects.data.common) // endregion // region DI - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) // endregion + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + // region Other Dependencies implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) implementation(deps.timber) // endregion diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt new file mode 100644 index 0000000000..28cfd01b9e --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -0,0 +1,20 @@ +package com.tangem.data.account.converter + +import javax.inject.Inject + +/** + * Container for converter factories related to accounts. + * + * @property accountsListCF factory for creating an account list converter + * @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter + * @property cryptoPortfolioCF factory for creating a crypto portfolio converter + * + * @constructor Creates an instance of the container with injected factories. + * +[REDACTED_AUTHOR] + */ +internal class AccountConverterFactoryContainer @Inject constructor( + val accountsListCF: AccountListConverter.Factory, + val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, + val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, +) \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt new file mode 100644 index 0000000000..a8ffea4d37 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.store + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +typealias AccountsResponseStore = DataStore + +/** + * Factory class for creating and managing instances of [AccountsResponseStore]. + * This class is responsible for creating a [DataStore] for each unique [UserWalletId]. + * + * @property context application context used to access the file system + * @property moshi moshi instance for JSON serialization and deserialization + * @property dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +internal class AccountsResponseStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + @OptIn(ExperimentalStdlibApi::class) + private val adapter by lazy { moshi.adapter() } + + private val createdDataStores = ConcurrentHashMap() + + /** + * Creates or retrieves an [AccountsResponseStore] for the given [UserWalletId]. + * + * @param userWalletId the unique identifier of the user's wallet + */ + fun create(userWalletId: UserWalletId): AccountsResponseStore { + return createdDataStores.computeIfAbsent(userWalletId) { + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer(defaultValue = null, adapter = adapter), + produceFile = { context.dataStoreFile(fileName = "wallet_accounts_${userWalletId.stringValue}") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + } + + @VisibleForTesting + fun getAllStores(): Map = createdDataStores.toMap() + + @VisibleForTesting + fun clearStores() { + createdDataStores.clear() + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt new file mode 100644 index 0000000000..6c39f7181c --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt @@ -0,0 +1,91 @@ +package com.tangem.data.account.store + +import android.content.Context +import com.google.common.truth.Truth +import com.squareup.moshi.Moshi +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountsResponseStoreFactoryTest { + + private val context: Context = mockk() + private val moshi: Moshi = Moshi.Builder().build() + private val factory: AccountsResponseStoreFactory = AccountsResponseStoreFactory( + context = context, + moshi = moshi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @AfterEach + fun setup() { + clearMocks(context) + factory.clearStores() + } + + @Test + fun `creates new data store for unique userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + val createdStore = factory.create(userWalletId = userWalletId) + + // Actual + val actual = factory.getAllStores() + + // Assert + Truth.assertThat(actual).containsExactly(userWalletId, createdStore) + } + + @Test + fun `reuses existing data store for same userWalletId`() { + val userWalletId = UserWalletId("011") + + // Arrange (first creation) + val firstStore = factory.create(userWalletId = userWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(userWalletId, firstStore) + + // Arrange (second creation) + val secondStore = factory.create(userWalletId = userWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + Truth.assertThat(actual2).containsExactly(userWalletId, secondStore) + Truth.assertThat(firstStore).isSameInstanceAs(secondStore) + } + + @Test + fun `creates separate data stores for different userWalletIds`() { + // Arrange (first creation) + val firstWalletId = UserWalletId("011") + val firstStore = factory.create(userWalletId = firstWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore) + + // Arrange (second creation) + val secondWalletId = UserWalletId("011") + val secondStore = factory.create(userWalletId = secondWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore) + Truth.assertThat(actual2).containsExactlyEntriesIn(expected) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 5c61ebc3f4..03934e051e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit +import com.tangem.common.ui.account.toDomain import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.common.ui.account.toDomain import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -170,10 +170,14 @@ internal class AccountCreateEditModel @Inject constructor( it.updateDerivationIndex(derivationIndex = derivationIndex.value) } } - .onLeft { + .onLeft { cause -> handleError( error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, - params = mapOf("userWalletId" to userWalletId.stringValue), + message = cause.toString(), + params = mapOf( + "userWalletId" to userWalletId.stringValue, + "cause" to cause.toString(), + ), ) return@launch @@ -181,8 +185,12 @@ internal class AccountCreateEditModel @Inject constructor( } } - private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { - val exception = IllegalStateException(error.toString()) + private fun handleError( + error: AccountFeatureError, + message: String? = null, + params: Map = mapOf(), + ) { + val exception = IllegalStateException("$error. Cause: $message") Timber.e(exception) From 8ee35d12f8b97ac2abd473c88cb22b1e4eb02428 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Aug 2025 09:26:25 +0300 Subject: [PATCH 12/48] Updated on 2026-08-14 --- .../common/ui/userwallet/UserWalletItem.kt | 82 +++++++++++++------ .../converter/UserWalletItemUMConverter.kt | 3 +- .../ui/userwallet/state/UserWalletItemUM.kt | 2 + 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 31c66358b4..ad44dcf7dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -139,33 +139,59 @@ private fun NameAndInfo( ) } } - Text( - text = " $DOT ", - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - ) - AnimatedContent( - targetState = balance, - label = "Balance content", - ) { balance -> - val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + BalanceContent(balance) + } + } +} - if (balanceValue == null) { - TextShimmer( - style = TangemTheme.typography.caption2, - text = "aaaaa", - ) - } else { +@Composable +private fun BalanceContent(balance: UserWalletItemUM.Balance, modifier: Modifier = Modifier) { + AnimatedContent( + modifier = modifier, + targetState = balance, + label = "Balance content", + ) { balance -> + when (balance) { + UserWalletItemUM.Balance.Locked -> { + Icon( + modifier = Modifier + .padding(start = 4.dp, bottom = 2.dp, top = 2.dp) + .size(12.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_lock_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.Balance.NotShowing -> { + /** No balance and no dot */ + } + else -> { + Row { Text( - text = balanceValue, - style = TangemTheme.typography.caption2.applyBladeBrush( - isEnabled = isFlickering, - textColor = TangemTheme.colors.text.tertiary, - ), + text = " $DOT ", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, maxLines = 1, ) + + val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + + if (balanceValue == null) { + TextShimmer( + style = TangemTheme.typography.caption2, + text = "aaaaa", + ) + } else { + Text( + text = balanceValue, + style = TangemTheme.typography.caption2.applyBladeBrush( + isEnabled = isFlickering, + textColor = TangemTheme.colors.text.tertiary, + ), + maxLines = 1, + ) + } } } } @@ -246,9 +272,8 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair DASH_SIGN to false is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false - is UserWalletItemUM.Balance.Loading -> null to false - is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering + else -> null to false } } @@ -392,6 +417,15 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider UserWalletItemUM.Balance.Hidden userWallet.isLocked -> UserWalletItemUM.Balance.Locked + authMode -> UserWalletItemUM.Balance.NotShowing + isBalanceHidden -> UserWalletItemUM.Balance.Hidden balance == null -> UserWalletItemUM.Balance.Loading else -> { when (balance) { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index c0abb2b485..b31b4d5ddb 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -28,6 +28,8 @@ data class UserWalletItemUM( data object Hidden : Balance() + data object NotShowing : Balance() + data object Locked : Balance() data object Failed : Balance() From df5af9afab63c68a435bc739e5d5964db317a57c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Aug 2025 12:15:10 +0000 Subject: [PATCH 13/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 57e50cf4ac..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1148" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 20dad876f08cf65244ef6a929859517b7bcc6235 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 14:49:43 +0400 Subject: [PATCH 14/48] Updated on 2026-08-14 --- .../local/datastore/RuntimeStateStore.kt | 13 ++ .../account/store/ArchivedAccountsStore.kt | 68 +++++++++ .../store/ArchivedAccountsStoreFactory.kt | 37 +++++ .../store/ArchivedAccountsStoreFactoryTest.kt | 79 ++++++++++ .../store/ArchivedAccountsStoreTest.kt | 139 ++++++++++++++++++ 5 files changed, 336 insertions(+) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStore.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeStateStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeStateStore.kt index b28130b286..05f3c620af 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeStateStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeStateStore.kt @@ -14,11 +14,18 @@ interface RuntimeStateStore { /** Get flow of elements [T] */ fun get(): StateFlow + /** Get element [T] synchronously or null */ + suspend fun getSyncOrNull(): T? + /** Store [value] */ suspend fun store(value: T) + /** Update current value by [function] */ suspend fun update(function: (T) -> T) + /** Clear stored value */ + fun clear() + companion object { /** @@ -32,6 +39,8 @@ interface RuntimeStateStore { override fun get(): StateFlow = flow + override suspend fun getSyncOrNull(): T? = flow.value + override suspend fun store(value: T) { flow.value = value } @@ -39,6 +48,10 @@ interface RuntimeStateStore { override suspend fun update(function: (T) -> T) { flow.update(function) } + + override fun clear() { + flow.value = defaultValue + } } } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStore.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStore.kt new file mode 100644 index 0000000000..cbbe1ca0b9 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStore.kt @@ -0,0 +1,68 @@ +package com.tangem.data.account.store + +import androidx.annotation.VisibleForTesting +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.account.models.ArchivedAccount +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlin.time.Duration.Companion.seconds + +/** + * Store for managing archived accounts with support for data expiration + * + * @property runtimeStore the underlying runtime shared store for storing the list of archived accounts + * +[REDACTED_AUTHOR] + */ +internal class ArchivedAccountsStore( + private val runtimeStore: RuntimeStateStore?>, +) { + + private var timestamp: Long? = null + + /** Retrieves a flow of archived accounts, filtering out null values */ + fun get(): Flow> { + return runtimeStore.get() + .map { + if (isDataExpired()) null else it + } + .filterNotNull() + } + + /** Retrieves the list of archived accounts synchronously, or null if the data is expired */ + suspend fun getSyncOrNull(): List? { + if (isDataExpired()) return null + + return runtimeStore.getSyncOrNull() + } + + /** Stores the provided list of archived accounts [value] */ + suspend fun store(value: List) { + timestamp = System.currentTimeMillis() + + runtimeStore.store(value) + } + + private fun isDataExpired(): Boolean { + val currentTime = System.currentTimeMillis() + val storedTime = timestamp ?: return true + + return currentTime - storedTime >= EXPIRATION_DURATION_MS + } + + @VisibleForTesting + fun setTimestamp(time: Long) { + timestamp = time + } + + @VisibleForTesting + fun clear() { + timestamp = null + runtimeStore.clear() + } + + private companion object Companion { + val EXPIRATION_DURATION_MS = 120.seconds.inWholeMicroseconds + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt new file mode 100644 index 0000000000..e89a9fa130 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.data.account.store + +import androidx.annotation.VisibleForTesting +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.models.wallet.UserWalletId +import java.util.concurrent.ConcurrentHashMap + +/** + * Factory for creating and managing instances of [ArchivedAccountsStore]. + + * and reused for each unique [UserWalletId]. + * +[REDACTED_AUTHOR] + */ +internal class ArchivedAccountsStoreFactory { + + private val createdRuntimeStores = ConcurrentHashMap() + + /** + * Creates or retrieves an existing instance of [ArchivedAccountsStore] for the given [userWalletId]. + * + * @param userWalletId the unique identifier for the user wallet + */ + fun create(userWalletId: UserWalletId): ArchivedAccountsStore { + return createdRuntimeStores.computeIfAbsent(userWalletId) { + ArchivedAccountsStore(runtimeStore = RuntimeStateStore(defaultValue = null)) + } + } + + @VisibleForTesting + fun getAllStores(): Map = createdRuntimeStores.toMap() + + @VisibleForTesting + fun clearStores() { + createdRuntimeStores.clear() + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt new file mode 100644 index 0000000000..977c3c4169 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt @@ -0,0 +1,79 @@ +package com.tangem.data.account.store + +import com.google.common.truth.Truth +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchivedAccountsStoreFactoryTest { + + private val factory = ArchivedAccountsStoreFactory() + + @AfterEach + fun tearDownEach() { + factory.clearStores() + } + + @Test + fun `creates new store for unique userWalletId`() { + // Arrange + val userWalletId = UserWalletId("001") + val createdStore = factory.create(userWalletId) + + // Act + val actual = factory.getAllStores() + + // Assert + Truth.assertThat(actual).containsExactly(userWalletId, createdStore) + } + + @Test + fun `reuses existing data store for same userWalletId`() { + val userWalletId = UserWalletId("011") + + // Arrange (first creation) + val firstStore = factory.create(userWalletId = userWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(userWalletId, firstStore) + + // Arrange (second creation) + val secondStore = factory.create(userWalletId = userWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + Truth.assertThat(actual2).containsExactly(userWalletId, secondStore) + Truth.assertThat(firstStore).isSameInstanceAs(secondStore) + } + + @Test + fun `creates separate data stores for different userWalletIds`() { + // Arrange (first creation) + val firstWalletId = UserWalletId("011") + val firstStore = factory.create(userWalletId = firstWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore) + + // Arrange (second creation) + val secondWalletId = UserWalletId("011") + val secondStore = factory.create(userWalletId = secondWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore) + Truth.assertThat(actual2).containsExactlyEntriesIn(expected) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreTest.kt b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreTest.kt new file mode 100644 index 0000000000..70e6ddb2db --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreTest.kt @@ -0,0 +1,139 @@ +package com.tangem.data.account.store + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.time.Duration.Companion.seconds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchivedAccountsStoreTest { + + private val runtimeStore: RuntimeStateStore?> = RuntimeStateStore(defaultValue = null) + private val archivedAccountsStore: ArchivedAccountsStore = ArchivedAccountsStore(runtimeStore = runtimeStore) + + @AfterEach + fun tearDown() { + archivedAccountsStore.clear() + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Get { + + @Test + fun `get returns empty flow`() = runTest { + // Act + val actual = getEmittedValues(archivedAccountsStore.get()) + + // Assert + Truth.assertThat(actual).isEmpty() // nothing emmited + } + + @Test + fun `get returns flow with not expired data`() = runTest { + // Arrange + val archivedAccount = createArchivedAccount() + archivedAccountsStore.store(value = listOf(archivedAccount)) + + // Act + val actual = getEmittedValues(archivedAccountsStore.get()) + + // Assert + val expected = listOf(archivedAccount) + Truth.assertThat(actual).containsExactly(expected) + } + + @Test + fun `get returns flow with expired data`() = runTest { + // Arrange + archivedAccountsStore.store(value = listOf(createArchivedAccount())) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds) + + // Act + val actual = getEmittedValues(archivedAccountsStore.get()) + + // Assert + Truth.assertThat(actual).isEmpty() // nothing emmited + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetSyncOrnNull { + + @Test + fun `getSyncOrNull returns null`() = runTest { + // Act + val actual = archivedAccountsStore.getSyncOrNull() + + // Assert + Truth.assertThat(actual).isNull() + } + + @Test + fun `get returns flow with not expired data`() = runTest { + // Arrange + val archivedAccount = createArchivedAccount() + archivedAccountsStore.store(value = listOf(archivedAccount)) + + // Act + val actual = archivedAccountsStore.getSyncOrNull() + + // Assert + val expected = archivedAccount + Truth.assertThat(actual).containsExactly(expected) + } + + @Test + fun `get returns flow with expired data`() = runTest { + // Arrange + archivedAccountsStore.store(value = listOf(createArchivedAccount())) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds) + + // Act + val actual = archivedAccountsStore.getSyncOrNull() + + // Assert + Truth.assertThat(actual).isNull() + } + } + + @Test + fun store() = runTest { + // Arrange + val archivedAccount = createArchivedAccount() + + // Act + archivedAccountsStore.store(value = listOf(archivedAccount)) + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = archivedAccount + Truth.assertThat(actual).containsExactly(expected) + } + + private fun createArchivedAccount(): ArchivedAccount { + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = UserWalletId("011"), + derivationIndex = DerivationIndex.Main, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = DerivationIndex.Main, + tokensCount = 2, + networksCount = 1, + ) + } +} \ No newline at end of file From b7eda75279589ab3c0532ef37e83d66fc4c81335 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Aug 2025 11:52:14 +0300 Subject: [PATCH 15/48] Updated on 2026-08-14 --- .../tangem/features/biometry/impl/model/AskBiometryModel.kt | 2 +- .../tangem/feature/wallet/child/wallet/model/WalletModel.kt | 3 +++ .../child/wallet/model/WalletsUpdateActionResolver.kt | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 614b462e24..195b2c9069 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -111,7 +111,6 @@ internal class AskBiometryModel @Inject constructor( private suspend fun handleSuccessAllowing(userWallet: UserWallet) { walletsRepository.saveShouldSaveUserWallets(item = true) - settingsRepository.setShouldSaveAccessCodes(value = true) if (hotWalletFeatureToggles.isHotWalletEnabled) { walletsRepository.setUseBiometricAuthentication(value = true) @@ -120,6 +119,7 @@ internal class AskBiometryModel @Inject constructor( isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), ) } else { + settingsRepository.setShouldSaveAccessCodes(value = true) if (userWallet is UserWallet.Cold) { cardSdkConfigRepository.setAccessCodeRequestPolicy( isBiometricsRequestPolicy = userWallet.hasAccessCode, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 14324845f1..e7a00027c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -363,6 +363,9 @@ internal class WalletModel @Inject constructor( is WalletsUpdateActionResolver.Action.RenameWallets -> { stateHolder.update(transformer = RenameWalletsTransformer(renamedWallets = action.renamedWallets)) } + WalletsUpdateActionResolver.Action.EmptyWallets -> { + Timber.w("Wallets list is empty!") + } is WalletsUpdateActionResolver.Action.Unknown -> { Timber.w("Unable to perform action: $action") } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 48ee23027a..3e9a4d114b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -25,6 +25,10 @@ internal class WalletsUpdateActionResolver @Inject constructor( ) { fun resolve(wallets: List, currentState: WalletScreenState): Action { + if (wallets.isEmpty()) { + return Action.EmptyWallets + } + val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { /* Selected user wallet can be null after reset if remaining user wallets is locked */ return Action.Unknown @@ -319,6 +323,8 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } + data object EmptyWallets : Action() + data object Unknown : Action() } } \ No newline at end of file From c6b1638ea6412fbe4ddc9fbc4bf636839280ec8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Aug 2025 14:33:14 +0500 Subject: [PATCH 16/48] Updated on 2026-08-14 --- .../start/ui/AddExistingWalletStartContent.kt | 6 +++- .../hotwallet/common/ui/OptionBlock.kt | 31 +++---------------- .../walletbackup/entity/WalletBackupUM.kt | 1 + .../walletbackup/model/WalletBackupModel.kt | 5 +++ .../walletbackup/ui/WalletBackupContent.kt | 31 +++++++++++++++++-- .../model/WalletSettingsModel.kt | 2 +- 6 files changed, 45 insertions(+), 31 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index 6bf693e36b..184f5f924f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -62,7 +62,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi ) OptionBlock( modifier = Modifier - .padding(top = 24.dp), + .padding(top = 32.dp), backgroundColor = TangemTheme.colors.background.secondary, title = stringResourceSafe(R.string.wallet_import_seed_title), description = stringResourceSafe(R.string.wallet_import_seed_description), @@ -71,6 +71,8 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi enabled = true, ) OptionBlock( + modifier = Modifier + .padding(top = 8.dp), backgroundColor = TangemTheme.colors.background.secondary, title = stringResourceSafe(R.string.wallet_import_scan_title), description = stringResourceSafe(R.string.wallet_import_scan_description), @@ -99,6 +101,8 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi enabled = true, ) OptionBlock( + modifier = Modifier + .padding(top = 8.dp), backgroundColor = TangemTheme.colors.background.secondary, title = stringResourceSafe(R.string.wallet_import_google_drive_title), description = stringResourceSafe(R.string.wallet_import_google_drive_description), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index c41dba03e8..f76ab84370 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -1,6 +1,5 @@ package com.tangem.features.hotwallet.common.ui -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,8 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -30,33 +29,11 @@ internal fun OptionBlock( backgroundColor: Color, modifier: Modifier = Modifier, ) { - val backgroundColor by animateColorAsState( - targetValue = if (enabled) { - backgroundColor - } else { - backgroundColor.copy(alpha = DISABLED_COLORS_ALPHA) - }, - ) - val titleColor by animateColorAsState( - targetValue = if (enabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.primary1.copy(alpha = DISABLED_COLORS_ALPHA) - }, - ) - val descriptionColor by animateColorAsState( - targetValue = if (enabled) { - TangemTheme.colors.text.tertiary - } else { - TangemTheme.colors.text.tertiary.copy(alpha = DISABLED_COLORS_ALPHA) - }, - ) - Column( modifier = modifier .fillMaxWidth() - .padding(top = 8.dp) .clip(TangemTheme.shapes.roundedCornersXMedium) + .alpha(if (enabled) 1f else DISABLED_COLORS_ALPHA) .background( color = backgroundColor, shape = TangemTheme.shapes.roundedCornersXMedium, @@ -73,7 +50,7 @@ internal fun OptionBlock( .padding(end = 4.dp), text = title, style = TangemTheme.typography.subtitle1, - color = titleColor, + color = TangemTheme.colors.text.primary1, ) badge?.invoke() } @@ -82,7 +59,7 @@ internal fun OptionBlock( .padding(top = 4.dp), text = description, style = TangemTheme.typography.body2, - color = descriptionColor, + color = TangemTheme.colors.text.tertiary, ) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index 3bc262d1a9..457edf278a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -9,6 +9,7 @@ internal data class WalletBackupUM( val googleDriveStatus: BackupStatus, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, + val onHardwareWalletClick: () -> Unit, val backedUp: Boolean, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 9996c7fb7a..20c094ed28 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -52,6 +52,7 @@ internal class WalletBackupModel @Inject constructor( googleDriveStatus = BackupStatus.ComingSoon, onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, + onHardwareWalletClick = ::onHardwareWalletClick, backedUp = false, ), ) @@ -122,4 +123,8 @@ internal class WalletBackupModel @Inject constructor( uiMessageSender.send(makeBackupAtFirstAlertBS) } } + + private fun onHardwareWalletClick() { + router.push(AppRoute.UpgradeWallet(params.userWalletId)) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 671c8dd89a..aa68dc3e7d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -25,6 +26,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.extensions.resourceReference @OptIn(ExperimentalMaterial3Api::class) @@ -47,7 +49,8 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod .padding(horizontal = 16.dp), ) { OptionBlock( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(top = 8.dp), title = stringResourceSafe(R.string.hw_backup_seed_title), description = stringResourceSafe(R.string.hw_backup_seed_description), badge = { @@ -59,7 +62,8 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod ) OptionBlock( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(top = 8.dp), title = stringResourceSafe(R.string.hw_backup_google_drive_title), description = stringResourceSafe(R.string.hw_backup_google_drive_description), badge = { @@ -69,6 +73,26 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod enabled = state.googleDriveStatus != BackupStatus.ComingSoon, backgroundColor = TangemTheme.colors.background.primary, ) + + NetworkTitle( + title = { + Text( + modifier = Modifier, + text = stringResourceSafe(R.string.express_provider_recommended), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) + OptionBlock( + modifier = Modifier.fillMaxWidth(), + title = stringResourceSafe(R.string.hw_backup_hardware_title), + description = stringResourceSafe(R.string.hw_backup_hardware_description), + badge = null, + onClick = state.onHardwareWalletClick, + enabled = true, + backgroundColor = TangemTheme.colors.background.primary, + ) } } } @@ -97,6 +121,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider Date: Thu, 28 Aug 2025 19:15:47 +0000 Subject: [PATCH 17/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 57e50cf4ac..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1148" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 9e80fccbbda0bc411dcb2b1b8ea2b082921eff86 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 09:33:17 +0300 Subject: [PATCH 18/48] Updated on 2026-08-14 --- app/src/main/res/drawable/inset_splash.xml | 10 +++++----- app/src/main/res/values/colors.xml | 2 -- app/src/main/res/values/styles.xml | 2 +- .../core/ui/components/fields/PinTextField.kt | 18 ++++++++++-------- core/ui/src/main/res/drawable/ic_tangem_24.xml | 6 +++--- .../ui}/src/main/res/drawable/splash_logo.xml | 10 +++++----- core/ui/src/main/res/values-night/colors.xml | 4 ++++ core/ui/src/main/res/values/colors.xml | 4 ++++ .../features/welcome/impl/ui/WelcomePlain.kt | 4 ++-- 9 files changed, 34 insertions(+), 26 deletions(-) rename {app => core/ui}/src/main/res/drawable/splash_logo.xml (80%) create mode 100644 core/ui/src/main/res/values-night/colors.xml create mode 100644 core/ui/src/main/res/values/colors.xml diff --git a/app/src/main/res/drawable/inset_splash.xml b/app/src/main/res/drawable/inset_splash.xml index 1d66dc1301..2886ecbd00 100644 --- a/app/src/main/res/drawable/inset_splash.xml +++ b/app/src/main/res/drawable/inset_splash.xml @@ -1,10 +1,10 @@ + android:insetLeft="55dp" + android:insetRight="55dp" + android:insetTop="55dp" + android:insetBottom="55dp"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index db52974e28..bf4703e8e2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -26,6 +26,4 @@ #1E1E1E #656565 - #000000 - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2b866d0894..d91dbb1dbe 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index 214da97109..a3226e0b46 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR @@ -159,14 +158,17 @@ private fun CellDecoration( ) } } else { - Text( + Box( modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), - text = text, - style = TangemTheme.typography.h3, - color = color, - textAlign = TextAlign.Center, - lineHeight = 48.sp, - ) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = text, + style = TangemTheme.typography.h3, + color = color, + textAlign = TextAlign.Center, + ) + } } } } diff --git a/core/ui/src/main/res/drawable/ic_tangem_24.xml b/core/ui/src/main/res/drawable/ic_tangem_24.xml index b4ccb9cc5a..cd606d5acf 100644 --- a/core/ui/src/main/res/drawable/ic_tangem_24.xml +++ b/core/ui/src/main/res/drawable/ic_tangem_24.xml @@ -6,11 +6,11 @@ android:viewportHeight="24"> + android:fillColor="@color/icon_primary1"/> + android:fillColor="@color/icon_primary1"/> + android:fillColor="@color/icon_primary1"/> diff --git a/app/src/main/res/drawable/splash_logo.xml b/core/ui/src/main/res/drawable/splash_logo.xml similarity index 80% rename from app/src/main/res/drawable/splash_logo.xml rename to core/ui/src/main/res/drawable/splash_logo.xml index ef37dfda51..f95768bf95 100644 --- a/app/src/main/res/drawable/splash_logo.xml +++ b/core/ui/src/main/res/drawable/splash_logo.xml @@ -1,15 +1,15 @@ diff --git a/core/ui/src/main/res/values-night/colors.xml b/core/ui/src/main/res/values-night/colors.xml new file mode 100644 index 0000000000..6c7b1cabaa --- /dev/null +++ b/core/ui/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFFFF + \ No newline at end of file diff --git a/core/ui/src/main/res/values/colors.xml b/core/ui/src/main/res/values/colors.xml new file mode 100644 index 0000000000..8dd91f8060 --- /dev/null +++ b/core/ui/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FF1E1E1E + \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt index 5b80e192ee..9da4e13724 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -22,8 +22,8 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) { contentAlignment = Alignment.Center, ) { Icon( - modifier = Modifier.size(84.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + modifier = Modifier.size(82.dp + 11.dp), + imageVector = ImageVector.vectorResource(R.drawable.splash_logo), tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) From 665c0ff7631dd3ffbefec460fd067d008e50394b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 14:35:24 +0500 Subject: [PATCH 19/48] Updated on 2026-08-14 --- .../tangem/tap/di/hot/TangemHotSdkModule.kt | 6 ++++++ .../hot/DefaultHotMapDerivationsRepository.kt | 1 + ...Accessor.kt => DefaultHotWalletAccessor.kt} | 18 ++++++++++-------- .../data/wallets/hot/TangemHotWalletSigner.kt | 1 + .../domain/wallets/hot/HotWalletAccessor.kt | 10 ++++++++++ 5 files changed, 28 insertions(+), 8 deletions(-) rename data/wallets/src/main/java/com/tangem/data/wallets/hot/{HotWalletAccessor.kt => DefaultHotWalletAccessor.kt} (92%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessor.kt diff --git a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt index c21b55928b..53110dc9c9 100644 --- a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt +++ b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt @@ -1,5 +1,7 @@ package com.tangem.tap.di.hot +import com.tangem.data.wallets.hot.DefaultHotWalletAccessor +import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.hot.sdk.TangemHotSdk import com.tangem.tap.features.hot.TangemHotSDKProxy import dagger.Binds @@ -15,4 +17,8 @@ internal interface TangemHotSdkModule { @Binds @Singleton fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk + + @Binds + @Singleton + fun bindHotWalletAccessor(default: DefaultHotWalletAccessor): HotWalletAccessor } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index 3eb9431fc3..bdc3f7fdf2 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -11,6 +11,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.domain.wallets.usecase.BackendId import com.tangem.hot.sdk.model.DeriveWalletRequest import com.tangem.operations.derivation.ExtendedPublicKeysMap diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt similarity index 92% rename from data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index b5ca40b2de..796bc8d3ba 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -3,7 +3,7 @@ package com.tangem.data.wallets.hot import com.tangem.common.core.TangemSdkError import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.copy +import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.hot.sdk.TangemHotSdk @@ -11,22 +11,24 @@ import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* import javax.inject.Inject -class HotWalletAccessor @Inject constructor( +class DefaultHotWalletAccessor @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletPasswordRequester: HotWalletPasswordRequester, private val walletsRepository: WalletsRepository, -) { +) : HotWalletAccessor { - suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = + override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = hotSdkRequest(hotWalletId) { unlock -> tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign) } - suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse = - hotSdkRequest(hotWalletId) { unlock -> - tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request) - } + override suspend fun derivePublicKeys( + hotWalletId: HotWalletId, + request: DeriveWalletRequest, + ): DerivedPublicKeyResponse = hotSdkRequest(hotWalletId) { unlock -> + tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request) + } private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { val isAccessCodeRequired = walletsRepository.requireAccessCode() diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index b5bfc9da12..ccef106815 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.map import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.hot.sdk.model.DataToSign import com.tangem.operations.sign.SignData import dagger.assisted.Assisted diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessor.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessor.kt new file mode 100644 index 0000000000..18d9d792fe --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessor.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.* + +interface HotWalletAccessor { + + suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List + + suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse +} \ No newline at end of file From d48ba8a8bdd704e04858db80712de9cf5aad7e07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 14:52:25 +0400 Subject: [PATCH 20/48] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 7 +- core/config-toggles/build.gradle.kts | 87 +++- .../di/FeatureTogglesManagerModule.kt | 20 +- .../feature/FeatureTogglesManager.kt | 3 - .../feature/impl/DevFeatureTogglesManager.kt | 80 +-- .../feature/impl/ProdFeatureTogglesManager.kt | 31 +- .../storage/FeatureTogglesLocalStorage.kt | 31 ++ .../core/configtoggle/utils/CollectionExt.kt | 10 + .../manager/DevFeatureTogglesManagerTest.kt | 489 ++++++++++++++++++ .../manager/DevTogglesManagerTest.kt | 237 --------- .../manager/ProdFeatureTogglesManagerTest.kt | 155 ++++++ .../manager/ProdTogglesManagerTest.kt | 136 ----- .../local/preferences/PreferencesKeys.kt | 2 - 13 files changed, 829 insertions(+), 459 deletions(-) create mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt create mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt delete mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt create mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt delete mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 09c556cae1..a413d65b34 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -77,6 +77,7 @@ import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.* import org.rekotlin.Store +import timber.log.Timber import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository lateinit var store: Store @@ -289,13 +290,17 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat tangemAppLoggerInitializer.initialize() + Timber.i("APP STARTED") + if (BuildConfig.TESTER_MENU_ENABLED) { + Timber.i(featureTogglesManager.toString()) + } + foregroundActivityObserver = ForegroundActivityObserver() registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. runBlocking { awaitAll( - async { featureTogglesManager.init() }, async { excludedBlockchainsManager.init() }, ) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) diff --git a/core/config-toggles/build.gradle.kts b/core/config-toggles/build.gradle.kts index 96fb87b639..368af3bc77 100644 --- a/core/config-toggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -1,4 +1,6 @@ -import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import org.json.JSONArray plugins { alias(deps.plugins.android.library) @@ -9,8 +11,24 @@ plugins { id("configuration") } +buildscript { + dependencies { + classpath("com.squareup:kotlinpoet:1.15.0") + classpath("org.json:json:20231013") + } +} + android { namespace = "com.tangem.core.configtoggle" + sourceSets["main"].java.srcDir("build/generated/source/toggles") +} + +tasks.named("preBuild") { + dependsOn(generateFeatureToggles /*generateExcludedBlockchainToggles*/) +} + +tasks.withType().configureEach { + useJUnitPlatform() } dependencies { @@ -32,7 +50,72 @@ dependencies { implementation(projects.core.utils) testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) + testImplementation(projects.common.test) +} + +val generateFeatureToggles by tasks.registering { + generateToggles( + inputFilePath = "src/main/assets/configs/feature_toggles_config.json", + generatedFileName = "FeatureToggles", + ) +} + +val generateExcludedBlockchainToggles by tasks.registering { + generateToggles( + inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json", + generatedFileName = "ExcludedBlockchainToggles", + ) +} + +fun Task.generateToggles(inputFilePath: String, generatedFileName: String) { + val inputFile = file(inputFilePath) + val outputDir = file("build/generated/source/toggles") + + inputs.file(inputFile) + outputs.dir(outputDir) + + doLast { + val jsonText = inputFile.readText() + val jsonArray = JSONArray(jsonText) + + val entries = (0 until jsonArray.length()).map { i -> + val obj = jsonArray.getJSONObject(i) + val name = obj.getString("name") + val version = obj.getString("version") + CodeBlock.of("%S to %S", name, version) + } + + val mapInitializer = CodeBlock.builder() + .add("mapOf(\n") + .indent() + .apply { + entries.forEachIndexed { index, entry -> + add(entry) + if (index != entries.lastIndex) add(",\n") else add("\n") + } + } + .unindent() + .add(")") + .build() + + val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName) + .addKdoc("Generated from $inputFilePath") + .addProperty( + PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING)) + .initializer(mapInitializer) + .build() + ) + + val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName) + .addType(objectBuilder.build()) + .build() + + val outputPackageDir = File(outputDir, "") + outputPackageDir.mkdirs() + fileSpec.writeTo(outputPackageDir) + } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index fa5c04f6a1..79ad0221fc 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -5,16 +5,14 @@ import com.tangem.core.configtoggle.BuildConfig import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager -import com.tangem.core.configtoggle.storage.LocalTogglesStorage +import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage import com.tangem.core.configtoggle.version.DefaultVersionProvider -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.runBlocking import javax.inject.Singleton @Module @@ -25,29 +23,17 @@ internal object FeatureTogglesManagerModule { @Singleton fun provideFeatureTogglesManager( @ApplicationContext context: Context, - assetLoader: AssetLoader, appPreferencesStore: AppPreferencesStore, ): FeatureTogglesManager { - val localTogglesStorage = LocalTogglesStorage(assetLoader) val versionProvider = DefaultVersionProvider(context) return if (BuildConfig.TESTER_MENU_ENABLED) { DevFeatureTogglesManager( - localTogglesStorage = localTogglesStorage, - appPreferencesStore = appPreferencesStore, versionProvider = versionProvider, + featureTogglesLocalStorage = FeatureTogglesLocalStorage(appPreferencesStore), ) } else { - ProdFeatureTogglesManager( - localTogglesStorage = localTogglesStorage, - versionProvider = versionProvider, - ) - }.also { - // We need to initialize during the hilt graph creation - // in order to provide the feature toggles correctly to other dependencies. - runBlocking { - it.init() - } + ProdFeatureTogglesManager(versionProvider = versionProvider) } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt index 86a027924c..0296565727 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt @@ -7,9 +7,6 @@ package com.tangem.core.configtoggle.feature */ interface FeatureTogglesManager { - /** Initialize manager */ - suspend fun init() - /** Check feature toggle availability by name [name] */ fun isFeatureEnabled(name: String): Boolean } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index ce0d035439..6facf5dc8b 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -1,77 +1,77 @@ package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting +import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager -import com.tangem.core.configtoggle.storage.TogglesStorage -import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage +import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.version.VersionProvider -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject +import kotlinx.coroutines.runBlocking +import java.util.Locale +import kotlin.properties.Delegates /** * Feature toggles manager implementation in DEV build * - * @property localTogglesStorage local feature toggles storage - * @property appPreferencesStore application local store * @property versionProvider application version provider + * @property featureTogglesLocalStorage local storage for feature toggles */ internal class DevFeatureTogglesManager( - private val localTogglesStorage: TogglesStorage, - private val appPreferencesStore: AppPreferencesStore, private val versionProvider: VersionProvider, + private val featureTogglesLocalStorage: FeatureTogglesLocalStorage, ) : MutableFeatureTogglesManager { - private var featureTogglesMap: MutableMap? = null - private var localFeatureTogglesMap: Map? = null + private var fileFeatureTogglesMap: Map = getFileFeatureToggles() + private var featureTogglesMap: MutableMap by Delegates.notNull() - override suspend fun init() { - if (featureTogglesMap != null && localFeatureTogglesMap != null) { - return // Already initialized - } + init { + val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() } - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - - val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( - key = PreferencesKeys.FEATURE_TOGGLES_KEY, - ) ?: emptyMap() - - val localFeatureToggles = localTogglesStorage.toggles - .associateToggles(currentVersion = versionProvider.get().orEmpty()) - - localFeatureTogglesMap = localFeatureToggles - - featureTogglesMap = localFeatureToggles + featureTogglesMap = fileFeatureTogglesMap .mapValues { resultToggle -> savedFeatureToggles[resultToggle.key] ?: resultToggle.value } .toMutableMap() } - override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] == true - override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap + override fun getFeatureToggles(): Map = featureTogglesMap - override fun getFeatureToggles(): Map = featureTogglesMap!! + override fun isMatchLocalConfig(): Boolean = featureTogglesMap == fileFeatureTogglesMap override suspend fun changeToggle(name: String, isEnabled: Boolean) { - featureTogglesMap!![name] ?: return - featureTogglesMap!![name] = isEnabled - appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!) + featureTogglesMap[name] ?: return + featureTogglesMap[name] = isEnabled + featureTogglesLocalStorage.store(value = featureTogglesMap) } override suspend fun recoverLocalConfig() { - featureTogglesMap = localFeatureTogglesMap!!.toMutableMap() - appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!) + featureTogglesMap = fileFeatureTogglesMap.toMutableMap() + featureTogglesLocalStorage.store(value = fileFeatureTogglesMap) + } + + override fun toString(): String { + return buildString { + append("DevFeatureTogglesManager:\n") + append("|------------------------------------------|-----------|\n") + append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled")) + append("|------------------------------------------|-----------|\n") + featureTogglesMap.entries.forEachIndexed { index, (name, isEnabled) -> + append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled)) + } + append("|------------------------------------------|-----------|") + } + } + + private fun getFileFeatureToggles(): Map { + val appVersion = versionProvider.get() + + return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun setFeatureToggles(map: MutableMap) { featureTogglesMap = map } - - private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map) { - storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value) - } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index 54fe7a011d..261ba22e8e 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -1,41 +1,30 @@ package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting +import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.core.configtoggle.storage.TogglesStorage -import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.version.VersionProvider /** * Feature toggles manager implementation in PROD build * - * @property localTogglesStorage local feature toggles storage - * @property versionProvider application version provider + * @property versionProvider application version provider */ internal class ProdFeatureTogglesManager( - private val localTogglesStorage: TogglesStorage, private val versionProvider: VersionProvider, ) : FeatureTogglesManager { - private var featureToggles: Map? = null + private val featureToggles: Map = getFileFeatureToggles() - override suspend fun init() { - if (featureToggles != null) { - return // Already initialized - } + override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - featureToggles = localTogglesStorage.toggles - .associateToggles(currentVersion = versionProvider.get() ?: "") + private fun getFileFeatureToggles(): Map { + val appVersion = versionProvider.get() + + return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion) } - override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getProdFeatureToggles() = featureToggles!! - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun setProdFeatureToggles(map: Map) { - featureToggles = map - } + fun getProdFeatureToggles() = featureToggles } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt new file mode 100644 index 0000000000..7ab7d42891 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt @@ -0,0 +1,31 @@ +package com.tangem.core.configtoggle.storage + +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.storeObjectMap + +/** + * Local storage for feature toggles + * + * @property appPreferencesStore app preferences store + * +[REDACTED_AUTHOR] + */ +internal class FeatureTogglesLocalStorage( + private val appPreferencesStore: AppPreferencesStore, +) { + + suspend fun getSyncOrEmpty(): Map { + return appPreferencesStore.getObjectMapSync(key = FEATURE_TOGGLES_KEY) + } + + suspend fun store(value: Map) { + appPreferencesStore.storeObjectMap(key = FEATURE_TOGGLES_KEY, value = value) + } + + private companion object { + + val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt index 68ee66b88c..d3e54bcac1 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt @@ -10,4 +10,14 @@ internal fun List.associateToggles(currentVersion: String): Map.defineTogglesAvailability(appVersion: String?): Map { + return if (appVersion == null) { + mapValues { false } + } else { + mapValues { (_, version) -> + VersionAvailabilityContract(currentVersion = appVersion, localVersion = version) + } + } } \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..e91cc5d705 --- /dev/null +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt @@ -0,0 +1,489 @@ +package com.tangem.core.configtoggle.manager + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager +import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel +import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage +import com.tangem.core.configtoggle.version.VersionProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DevFeatureTogglesManagerTest { + + private val versionProvider = mockk() + private val featureTogglesLocalStorage = mockk(relaxUnitFun = true) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @BeforeAll + fun setupAll() { + val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0") + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns featureToggles + } + + @AfterAll + fun tearDownAll() { + unmockkObject(FeatureToggles) + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, featureTogglesLocalStorage) + } + + @Test + fun `successfully initialize manager`() = runTest { + // Arrange + val appVersion = "1.0.0" + val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + // Act + val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + + // Assert + val expected = savedFeatureToggles + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + + @Test + fun `successfully initialize manager if versionProvider returns null`() = runTest { + // Arrange + val appVersion = null + val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + // Act + val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + + // Assert + val expected = savedFeatureToggles + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + + @Test + fun `successfully initialize manager if versionProvider returns empty string`() = runTest { + // Arrange + val appVersion = "" + val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + // Act + val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + + // Assert + val expected = savedFeatureToggles + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + + @Test + fun `failure initialize manager if versionProvider throws exception`() = runTest { + // Arrange + val exception = Exception("Test exception") + every { versionProvider.get() } throws exception + + // Act + val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) } + .exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { versionProvider.get() } + coVerify(inverse = true) { featureTogglesLocalStorage.getSyncOrEmpty() } + } + + @Test + fun `successfully initialize manager if storage returns empty map`() = runTest { + // Arrange + val appVersion = "1.0.0" + val savedFeatureToggles = emptyMap() + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + // Act + val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + + // Assert + val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + + @Test + fun `successfully initialize manager if storage returns unknown toggles`() = runTest { + // Arrange + val appVersion = "1.0.0" + val savedFeatureToggles = mapOf("TOGGLE_3" to true) + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + // Act + val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + + // Assert + val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + + @Test + fun `failure initialize manager if storage throws exception`() = runTest { + // Arrange + val appVersion = "1.0.0" + val exception = Exception("Test exception") + + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception + + // Act + val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) } + .exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsFeatureEnabled { + + private lateinit var manager: DevFeatureTogglesManager + + @BeforeAll + fun setupAll() { + every { versionProvider.get() } returns "1.0.0" + + val featureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", + "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + ) + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns featureToggles + + val savedFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to true, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + } + + @AfterAll + fun tearDownAll() { + clearMocks(versionProvider, featureTogglesLocalStorage) + unmockkObject(FeatureToggles) + } + + @ParameterizedTest + @ProvideTestModels + fun isFeatureEnabled(model: IsFeatureEnabledModel) { + // Act + val actual = manager.isFeatureEnabled(name = model.name) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false), + IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true), + IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false), + IsFeatureEnabledModel(name = "", expected = false), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsMatchLocalConfig { + + @ParameterizedTest + @ProvideTestModels + fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns model.fileFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap() + + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply { + setFeatureToggles(model.storedFeatureToggles.toMutableMap()) + } + + // Act + val actual = manager.isMatchLocalConfig() + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + + clearMocks(versionProvider, featureTogglesLocalStorage) + unmockkObject(FeatureToggles) + } + + private fun provideTestModels() = listOf( + IsMatchLocalConfigModel( + fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), + storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false), + expected = true, + ), + IsMatchLocalConfigModel( + fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), + storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true), + expected = false, + ), + IsMatchLocalConfigModel( + fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), + storedFeatureToggles = mapOf("TOGGLE_1" to true), + expected = false, + ), + IsMatchLocalConfigModel( + fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), + storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false), + expected = false, + ), + IsMatchLocalConfigModel( + fileFeatureToggles = emptyMap(), + storedFeatureToggles = emptyMap(), + expected = true, + ), + IsMatchLocalConfigModel( + fileFeatureToggles = emptyMap(), + storedFeatureToggles = mapOf("TOGGLE_1" to true), + expected = false, + ), + ) + } + + data class IsMatchLocalConfigModel( + val fileFeatureToggles: Map, + val storedFeatureToggles: Map, + val expected: Boolean, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetFeatureToggles { + + @Test + fun getFeatureToggles() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + + val fileFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", + "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + ) + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns fileFeatureToggles + + val savedFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to true, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + + // Act + val actual = manager.getFeatureToggles() + + // Assert + val expected = savedFeatureToggles + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + } + + clearMocks(versionProvider, featureTogglesLocalStorage) + unmockkObject(FeatureToggles) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ChangeToggle { + + @ParameterizedTest + @ProvideTestModels + fun changeToggle(model: ChangeToggleModel) = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns model.initialToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap() + + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + + // Act + manager.changeToggle(name = model.name, isEnabled = model.isEnabled) + val actual = manager.getFeatureToggles() + + // Assert + val expected = model.expectedToggles + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + + if (model.expectedStoreSaving) { + featureTogglesLocalStorage.store(model.expectedToggles) + } + } + + clearMocks(versionProvider, featureTogglesLocalStorage) + unmockkObject(FeatureToggles) + } + + private fun provideTestModels() = listOf( + ChangeToggleModel( + initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), + name = "TOGGLE_1", + isEnabled = false, + expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false), + expectedStoreSaving = true, + ), + ChangeToggleModel( + initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), + name = "TOGGLE_2", + isEnabled = true, + expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true), + expectedStoreSaving = true, + ), + ChangeToggleModel( + initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), + name = "TOGGLE_3", + isEnabled = true, + expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false), + expectedStoreSaving = false, + ), + ChangeToggleModel( + initialToggles = emptyMap(), + name = "TOGGLE_1", + isEnabled = true, + expectedToggles = emptyMap(), + expectedStoreSaving = false, + ), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class RecoverLocalConfig { + + @Test + fun recoverLocalConfig() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + + val fileFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", + "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + ) + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns fileFeatureToggles + + val savedFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to true, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + + // Act + manager.recoverLocalConfig() + val actual = manager.getFeatureToggles() + + // Assert + val expected = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to true, + ) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { + versionProvider.get() + featureTogglesLocalStorage.getSyncOrEmpty() + featureTogglesLocalStorage.store(expected) + } + + clearMocks(versionProvider, featureTogglesLocalStorage) + unmockkObject(FeatureToggles) + } + } + + data class ChangeToggleModel( + val initialToggles: Map, + val name: String, + val isEnabled: Boolean, + val expectedToggles: Map, + val expectedStoreSaving: Boolean, + ) +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt deleted file mode 100644 index a4c3d6d7aa..0000000000 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.tangem.core.configtoggle.manager - -import android.annotation.SuppressLint -import com.google.common.truth.Truth -import com.squareup.moshi.Moshi -import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager -import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants -import com.tangem.core.configtoggle.storage.ConfigToggle -import com.tangem.core.configtoggle.storage.TogglesStorage -import com.tangem.core.configtoggle.utils.associateToggles -import com.tangem.core.configtoggle.version.VersionProvider -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.getSyncOrNull -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.collections.set - -/** -[REDACTED_AUTHOR] - */ -@SuppressLint("CheckResult") -internal class DevTogglesManagerTest { - - private val localTogglesStorage = mockk() - private val appPreferenceStore = AppPreferencesStore( - moshi = Moshi.Builder().build(), - dispatchers = TestingCoroutineDispatcherProvider(), - preferencesDataStore = mockk(relaxed = true), - ) - private val versionProvider = mockk() - private val manager = DevFeatureTogglesManager( - localTogglesStorage = localTogglesStorage, - appPreferencesStore = appPreferenceStore, - versionProvider = versionProvider, - ) - - @Test - fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest { - val currentVersion = "0.1.0" - - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - coEvery { - appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) - } returns savedFeatureTogglesMap - coEvery { localTogglesStorage.toggles } returns localFeatureToggles - coEvery { versionProvider.get() } returns currentVersion - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - val expected = localFeatureToggles - .associateToggles(currentVersion) - .mapValues { resultToggle -> - savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value - } - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `successfully initialize storage if shared prefs kept empty list`() = runTest { - val currentVersion = "0.1.0" - - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - coEvery { - appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) - } returns emptyMap() - coEvery { localTogglesStorage.toggles } returns localFeatureToggles - coEvery { versionProvider.get() } returns currentVersion - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - val expected = localFeatureToggles - .associateToggles(currentVersion) - .mapValues { resultToggle -> - savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value - } - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest { - val currentVersion = "0.1.0" - - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - coEvery { - appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) - } returns null - coEvery { localTogglesStorage.toggles } returns localFeatureToggles - coEvery { versionProvider.get() } returns currentVersion - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - val expected = localFeatureToggles - .associateToggles(currentVersion) - .mapValues(Map.Entry::value) - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `successfully initialize storage if versionProvider returns null`() = runTest { - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles - coEvery { localTogglesStorage.toggles } returns localFeatureToggles - coEvery { versionProvider.get() } returns null - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - val expected = localFeatureToggles - .associateToggles(currentVersion = "") - .mapValues { resultToggle -> - savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value - } - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `get feature availability if feature toggle exists`() { - val featureToggles = mutableMapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to true, - "ACTIVE2_TEST_FEATURE_ENABLED" to true, - ) - manager.setFeatureToggles(featureToggles) - - val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED") - - Truth.assertThat(actual).isTrue() - } - - @Test - fun `get feature availability if feature toggle doesn't exists`() { - val featureToggles = mutableMapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to true, - ) - manager.setFeatureToggles(featureToggles) - - val actual = manager.isFeatureEnabled(name = "") - - Truth.assertThat(actual).isFalse() - } - - @Test - fun getFeatureToggles() { - val expected = mutableMapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - - manager.setFeatureToggles(expected) - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `change toggle that contains in map`() = runTest { - val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED" - val resultMap = mutableMapOf( - changeableToggleName to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - - manager.setFeatureToggles(resultMap) - - manager.changeToggle(changeableToggleName, true) - - resultMap[changeableToggleName] = true - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap) - } - - @Test - fun `change toggle that doesn't contains in map`() = runTest { - val resultMap = mutableMapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - - manager.setFeatureToggles(resultMap) - - manager.changeToggle("FEATURE_TOGGLE", true) - - Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap) - } - - private companion object { - - val savedFeatureToggles = """ - [ - { - "name": "INACTIVE_TEST_FEATURE_ENABLED", - "version": "undefined" - }, - { - "name": "ACTIVE2_TEST_FEATURE_ENABLED", - "version": "1.0.0" - } - ] - """.trimIndent() - - val savedFeatureTogglesMap = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - - val localFeatureToggles = listOf( - ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), - ) - } -} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..ffef24d22e --- /dev/null +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -0,0 +1,155 @@ +package com.tangem.core.configtoggle.manager + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager +import com.tangem.core.configtoggle.version.VersionProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ProdFeatureTogglesManagerTest { + + private val versionProvider = mockk() + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @BeforeAll + fun setupAll() { + val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0") + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns featureToggles + } + + @AfterAll + fun tearDownAll() { + unmockkObject(FeatureToggles) + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider) + } + + @Test + fun `successfully initialize storage`() = runTest { + // Arrange + val appVersion = "1.0.0" + every { versionProvider.get() } returns appVersion + + // Act + val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + + // Assert + val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `successfully initialize storage if versionProvider returns null`() = runTest { + // Arrange + val appVersion = null + every { versionProvider.get() } returns appVersion + + // Act + val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + + // Assert + val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `successfully initialize storage if versionProvider returns empty string`() = runTest { + // Arrange + val appVersion = "" + every { versionProvider.get() } returns appVersion + + // Act + val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + + // Assert + val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false) + Truth.assertThat(actual).containsExactlyEntriesIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `failure initialize storage if versionProvider throws exception`() = runTest { + // Arrange + val exception = Exception("Test exception") + every { versionProvider.get() } throws exception + + // Act + val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { versionProvider.get() } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsFeatureEnabled { + + private lateinit var manager: ProdFeatureTogglesManager + + @BeforeAll + fun setupAll() { + every { versionProvider.get() } returns "1.0.0" + + val featureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", + "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + ) + + mockkObject(FeatureToggles) + every { FeatureToggles.values } returns featureToggles + + manager = ProdFeatureTogglesManager(versionProvider) + } + + @AfterAll + fun tearDownAll() { + clearMocks(versionProvider) + unmockkObject(FeatureToggles) + } + + @ParameterizedTest + @ProvideTestModels + fun isFeatureEnabled(model: IsFeatureEnabledModel) { + // Act + val actual = manager.isFeatureEnabled(name = model.name) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true), + IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false), + IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false), + IsFeatureEnabledModel(name = "", expected = false), + ) + } + + data class IsFeatureEnabledModel(val name: String, val expected: Boolean) +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt deleted file mode 100644 index 6453d3eb4d..0000000000 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.tangem.core.configtoggle.manager - -import android.content.pm.PackageManager -import com.google.common.truth.Truth -import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants -import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager -import com.tangem.core.configtoggle.storage.ConfigToggle -import com.tangem.core.configtoggle.storage.TogglesStorage -import com.tangem.core.configtoggle.utils.associateToggles -import com.tangem.core.configtoggle.version.VersionProvider -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.Test - -/** -[REDACTED_AUTHOR] - */ -internal class ProdTogglesManagerTest { - - private val localTogglesStorage = mockk() - private val versionProvider = mockk() - private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider) - - @Test - fun `successfully initialize storage`() = runTest { - val currentVersion = "1.0.0" - - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - every { localTogglesStorage.toggles } returns localFeatureToggles - every { versionProvider.get() } returns currentVersion - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - val expected = localFeatureToggles.associateToggles(currentVersion) - Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected) - } - - @Test - fun `successfully initialize storage if versionProvider returns null`() = runTest { - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - every { localTogglesStorage.toggles } returns localFeatureToggles - every { versionProvider.get() } returns null - - manager.init() - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - - Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles) - } - - @Test - fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest { - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - every { localTogglesStorage.toggles } throws IllegalStateException( - "Property featureToggles should be initialized before get.", - ) - - runCatching { manager.init() } - .onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") } - .onFailure { - Truth - .assertThat(it) - .hasMessageThat() - .contains("Property featureToggles should be initialized before get.") - - Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) - } - - coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } - verifyAll(inverse = true) { versionProvider.get() } - } - - @Test - fun `failure initialize storage if versionProvider throws exception`() = runTest { - coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs - every { localTogglesStorage.toggles } returns localFeatureToggles - every { versionProvider.get() } throws PackageManager.NameNotFoundException() - - runCatching { manager.init() } - .onSuccess { throw IllegalStateException("versionProvider should throws exception") } - .onFailure { - Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java) - } - - coVerifyOrder { - localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - versionProvider.get() - } - } - - @Test - fun `get feature availability if feature toggle exists`() { - val featureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to true, - "ACTIVE2_TEST_FEATURE_ENABLED" to true, - ) - manager.setProdFeatureToggles(featureToggles) - - val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED") - - Truth.assertThat(actual).isTrue() - } - - @Test - fun `get feature availability if feature toggle doesn't exists`() { - val featureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to true, - ) - manager.setProdFeatureToggles(featureToggles) - - val actual = manager.isFeatureEnabled(name = "") - - Truth.assertThat(actual).isFalse() - } - - private companion object { - val localFeatureToggles = listOf( - ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), - ) - - val disabledFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 06a5f51102..162d75a40f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -56,8 +56,6 @@ object PreferencesKeys { val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } - val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } - val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } From 3d5a8a55ed9e060a9f1d0ed2f6a837d121ac0511 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Aug 2025 14:03:04 +0400 Subject: [PATCH 21/48] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 3 +- .../AccountConverterFactoryContainer.kt | 22 +- .../data/account/di/AccountDataModule.kt | 20 +- .../DefaultAccountsCRUDRepository.kt | 150 ++-- .../store/ArchivedAccountsStoreFactory.kt | 2 +- .../DefaultAccountsCRUDRepositoryTest.kt | 664 ++++++++++++++++++ .../store/ArchivedAccountsStoreFactoryTest.kt | 2 +- .../repository/AccountsCRUDRepository.kt | 10 +- .../usecase/AddCryptoPortfolioUseCase.kt | 4 +- .../usecase/ArchiveCryptoPortfolioUseCase.kt | 2 +- .../usecase/GetArchivedAccountsUseCase.kt | 8 +- .../GetUnoccupiedAccountIndexUseCase.kt | 5 + .../usecase/RecoverCryptoPortfolioUseCase.kt | 4 +- .../usecase/UpdateCryptoPortfolioUseCase.kt | 2 +- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 20 +- .../ArchiveCryptoPortfolioUseCaseTest.kt | 20 +- .../usecase/GetArchivedAccountsUseCaseTest.kt | 16 +- .../GetUnoccupiedAccountIndexUseCaseTest.kt | 3 +- .../RecoverCryptoPortfolioUseCaseTest.kt | 44 +- .../UpdateCryptoPortfolioUseCaseTest.kt | 32 +- 20 files changed, 899 insertions(+), 134 deletions(-) create mode 100644 data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index be6fe202e7..9a557cd922 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -154,7 +154,8 @@ interface TangemTechApi { suspend fun saveWalletAccounts( @Path("walletId") walletId: String, @Header("If-Match") ifMatch: String, - ): ApiResponse + @Body body: SaveWalletAccountsResponse, + ): ApiResponse @GET("/v1/wallets/{walletId}/accounts/archived") suspend fun getWalletArchivedAccounts( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt index 28cfd01b9e..e8bcef8cce 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -1,5 +1,7 @@ package com.tangem.data.account.converter +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWalletId import javax.inject.Inject /** @@ -14,7 +16,21 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class AccountConverterFactoryContainer @Inject constructor( - val accountsListCF: AccountListConverter.Factory, val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, - val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, -) \ No newline at end of file + private val accountsListCF: AccountListConverter.Factory, + private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, + private val userWalletsStore: UserWalletsStore, +) { + + fun createAccountListConverter(userWalletId: UserWalletId): AccountListConverter { + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + + return accountsListCF.create(userWallet) + } + + fun createCryptoPortfolioConverter(userWalletId: UserWalletId): CryptoPortfolioConverter { + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + + return cryptoPortfolioCF.create(userWallet) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index e05c402f68..df872e31dc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -1,9 +1,13 @@ package com.tangem.data.account.di +import com.tangem.data.account.converter.AccountConverterFactoryContainer import com.tangem.data.account.repository.DefaultAccountsCRUDRepository -import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -16,10 +20,20 @@ internal object AccountDataModule { @Provides @Singleton - fun provideAccountsCRUDRepository(userWalletsStore: UserWalletsStore): AccountsCRUDRepository { + fun provideAccountsCRUDRepository( + tangemTechApi: TangemTechApi, + accountsResponseStoreFactory: AccountsResponseStoreFactory, + userWalletsStore: UserWalletsStore, + accountConverterFactoryContainer: AccountConverterFactoryContainer, + dispatchers: CoroutineDispatcherProvider, + ): AccountsCRUDRepository { return DefaultAccountsCRUDRepository( - runtimeStore = RuntimeSharedStore(), + tangemTechApi = tangemTechApi, + accountsResponseStoreFactory = accountsResponseStoreFactory, + archivedAccountsStoreFactory = ArchivedAccountsStoreFactory, userWalletsStore = userWalletsStore, + convertersContainer = accountConverterFactoryContainer, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a9fba30f57..3d148930d5 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -1,90 +1,150 @@ package com.tangem.data.account.repository import arrow.core.Option -import arrow.core.Option.Companion.catch -import arrow.core.none import arrow.core.raise.option -import com.tangem.datasource.local.datastore.RuntimeSharedStore +import arrow.core.toOption +import com.tangem.data.account.converter.AccountConverterFactoryContainer +import com.tangem.data.account.converter.ArchivedAccountConverter +import com.tangem.data.account.converter.SaveWalletAccountsResponseConverter +import com.tangem.data.account.store.AccountsResponseStore +import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.store.ArchivedAccountsStore +import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.account.* +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext /** [REDACTED_AUTHOR] */ -// TODO: [REDACTED_JIRA] internal class DefaultAccountsCRUDRepository( - private val runtimeStore: RuntimeSharedStore>, + private val tangemTechApi: TangemTechApi, + private val accountsResponseStoreFactory: AccountsResponseStoreFactory, + private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory, private val userWalletsStore: UserWalletsStore, + private val convertersContainer: AccountConverterFactoryContainer, + private val dispatchers: CoroutineDispatcherProvider, ) : AccountsCRUDRepository { - override suspend fun getAccounts(userWalletId: UserWalletId): Option = catch { - runtimeStore.getSyncOrNull() - ?.firstOrNull { it.userWallet.walletId == userWalletId } - ?: return none() + private val saveAccountsMutex = Mutex() + + override suspend fun getAccountListSync(userWalletId: UserWalletId): Option = option { + val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId) + + ensureNotNull(accountListResponse) + + val converter = convertersContainer.createAccountListConverter(userWalletId = userWalletId) + converter.convert(value = accountListResponse) } - override suspend fun getAccount(accountId: AccountId): Option = catch { - runtimeStore.getSyncOrNull().orEmpty() - .flatMap { it.accounts } - .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio - ?: return none() + override suspend fun getAccountSync(accountId: AccountId): Option = option { + val userWalletId = accountId.userWalletId + + val accountResponse = getAccountsResponseSync(userWalletId = userWalletId) + ?.accounts?.firstOrNull { it.id == accountId.value } + + ensureNotNull(accountResponse) + + val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = userWalletId) + converter.convert(value = accountResponse) } - override suspend fun getArchivedAccount(accountId: AccountId): Option = option { - createMockArchivedAccount(userWalletId = accountId.userWalletId) + override suspend fun getArchivedAccountSync(accountId: AccountId): Option { + val store = getArchivedAccountsStore(userWalletId = accountId.userWalletId) + + return store.getSyncOrNull() + ?.firstOrNull { it.accountId == accountId } + .toOption() } - override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { - listOf( - createMockArchivedAccount(userWalletId), - ) + override suspend fun getArchivedAccountListSync(userWalletId: UserWalletId): Option> { + val store = getArchivedAccountsStore(userWalletId = userWalletId) + + return store.getSyncOrNull().toOption() } override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { - return flow { - getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() - } + val store = getArchivedAccountsStore(userWalletId = userWalletId) + + return store.get() } - override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) { + val response = withContext(dispatchers.io) { + tangemTechApi.getWalletArchivedAccounts(walletId = userWalletId.stringValue).getOrThrow() + } + + val store = getArchivedAccountsStore(userWalletId = userWalletId) + val converter = ArchivedAccountConverter(userWalletId = userWalletId) + + val archivedAccounts = converter.convertList(input = response.accounts) + + store.store(value = archivedAccounts) + } override suspend fun saveAccounts(accountList: AccountList) { - runtimeStore.update(emptyList()) { - it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } + saveAccountsMutex.withLock { + val store = getAccountsResponseStore(userWalletId = accountList.userWallet.walletId) + + val version = store.data.firstOrNull()?.wallet?.version ?: 0 + val body = SaveWalletAccountsResponseConverter.convert(value = accountList) + + withContext(dispatchers.io) { + tangemTechApi.saveWalletAccounts( + walletId = accountList.userWallet.walletId.stringValue, + ifMatch = version.toString(), + body = body, + ) + .getOrThrow() + } + + val converter = convertersContainer.getWalletAccountsResponseCF.create( + userWallet = accountList.userWallet, + version = version, + ) + + val accountsResponse = converter.convert(value = accountList) + + store.updateData { accountsResponse } } } - override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int { - val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1 + override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option = option { + val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId) - return activeAccountsCount + 1 + ensureNotNull(accountListResponse) + + return accountListResponse.wallet.totalAccounts.toOption() } override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } - private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { - val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? { + val store = getAccountsResponseStore(userWalletId = userWalletId) + return store.data.firstOrNull() + } - return ArchivedAccount( - accountId = AccountId.forCryptoPortfolio( - userWalletId = userWalletId, - derivationIndex = derivationIndex, - ), - name = AccountName("Archived Account").getOrNull()!!, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = derivationIndex, - tokensCount = 2, - networksCount = 1, - ) + private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore { + return accountsResponseStoreFactory.create(userWalletId = userWalletId) + } + + private fun getArchivedAccountsStore(userWalletId: UserWalletId): ArchivedAccountsStore { + return archivedAccountsStoreFactory.create(userWalletId) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt index e89a9fa130..96e522270b 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/ArchivedAccountsStoreFactory.kt @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap * [REDACTED_AUTHOR] */ -internal class ArchivedAccountsStoreFactory { +internal object ArchivedAccountsStoreFactory { private val createdRuntimeStores = ConcurrentHashMap() diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt new file mode 100644 index 0000000000..0002eb553d --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -0,0 +1,664 @@ +package com.tangem.data.account.repository + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.account.converter.* +import com.tangem.data.account.store.AccountsResponseStore +import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.store.ArchivedAccountsStore +import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.Account.CryptoPortfolio +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* +import kotlin.time.Duration.Companion.minutes + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultAccountsCRUDRepositoryTest { + + private val tangemTechApi: TangemTechApi = mockk() + + private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk() + private val accountsResponseStore: AccountsResponseStore = mockk() + private val accountsResponseStoreFlow = MutableStateFlow(value = null) + + private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory = mockk() + private val archivedAccountsInnerStore = RuntimeStateStore?>(defaultValue = null) + private val archivedAccountsStore = ArchivedAccountsStore(runtimeStore = archivedAccountsInnerStore) + + private val userWalletsStore: UserWalletsStore = mockk() + + private val convertersContainer: AccountConverterFactoryContainer = mockk() + private val accountListConverter: AccountListConverter = mockk() + private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk() + + private val repository = DefaultAccountsCRUDRepository( + tangemTechApi = tangemTechApi, + accountsResponseStoreFactory = accountsResponseStoreFactory, + archivedAccountsStoreFactory = archivedAccountsStoreFactory, + userWalletsStore = userWalletsStore, + convertersContainer = convertersContainer, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val userWalletId = UserWalletId("011") + + @BeforeAll + fun setup() { + every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore + every { accountsResponseStore.data } returns accountsResponseStoreFlow + + every { convertersContainer.createAccountListConverter(userWalletId) } returns accountListConverter + every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + every { archivedAccountsStoreFactory.create(userWalletId) } returns archivedAccountsStore + } + + @AfterEach + fun tearDownEach() { + accountsResponseStoreFlow.value = null + archivedAccountsInnerStore.clear() + + clearMocks( + tangemTechApi, + archivedAccountsStoreFactory, + accountListConverter, + cryptoPortfolioConverter, + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetAccountListSync { + + @Test + fun `getAccounts should return None when account list response is null`() = runTest { + // Arrange + accountsResponseStoreFlow.value = null + + // Act + val actual = repository.getAccountListSync(userWalletId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + } + + verify(inverse = true) { accountListConverter.convert(value = any()) } + } + + @Test + fun `getAccounts should return AccountList when account list response is not null`() = runTest { + // Arrange + val response = mockk() + val accountList = mockk() + + accountsResponseStoreFlow.value = response + + every { accountListConverter.convert(response) } returns accountList + + // Act + val actual = repository.getAccountListSync(userWalletId) + + // Assert + val expected = accountList.toOption() + Truth.assertThat(actual).isEqualTo(expected) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + convertersContainer.createAccountListConverter(userWalletId = userWalletId) + accountListConverter.convert(response) + } + } + + @Test + fun `getAccounts should throw exception if converter throws exception`() = runTest { + // Arrange + val response = mockk() + mockk() + + accountsResponseStoreFlow.value = response + + val exception = Exception("Test error") + + every { accountListConverter.convert(response) } throws exception + + // Act + val actual = runCatching { repository.getAccountListSync(userWalletId) }.exceptionOrNull()!! + + // Assert + val expected = exception + Truth.assertThat(actual).isSameInstanceAs(expected) + Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + convertersContainer.createAccountListConverter(userWalletId = userWalletId) + accountListConverter.convert(response) + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetAccountSync { + + private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main) + + @Test + fun `getAccount should return None when account response is null`() = runTest { + // Arrange + val response = null + + accountsResponseStoreFlow.value = response + + // Act + val actual = repository.getAccountSync(accountId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + } + + verify(inverse = true) { convertersContainer.createCryptoPortfolioConverter(userWalletId = any()) } + } + + @Test + fun `getAccount should return None when accountDto is not found`() = runTest { + // Arrange + val response = mockk { + every { this@mockk.accounts } returns emptyList() + } + + accountsResponseStoreFlow.value = response + + // Act + val actual = repository.getAccountSync(accountId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + } + + verify(inverse = true) { convertersContainer.createCryptoPortfolioConverter(userWalletId = any()) } + } + + @Test + fun `getAccount should return Account_CryptoPortfolio when account response is not null`() = runTest { + // Arrange + val accountDTO = mockk { + every { this@mockk.id } returns accountId.value + } + + val response = mockk { + every { this@mockk.accounts } returns listOf(accountDTO) + } + + accountsResponseStoreFlow.value = response + + val cryptoPortfolio = mockk() + + every { cryptoPortfolioConverter.convert(accountDTO) } returns cryptoPortfolio + + // Act + val actual = repository.getAccountSync(accountId) + + // Assert + val expected = cryptoPortfolio.toOption() + Truth.assertThat(actual).isEqualTo(expected) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + convertersContainer.createCryptoPortfolioConverter(userWalletId) + cryptoPortfolioConverter.convert(accountDTO) + } + } + + @Test + fun `getAccount should throw exception if converter throws exception`() = runTest { + // Arrange + val accountDTO = mockk { + every { this@mockk.id } returns accountId.value + } + + val response = mockk { + every { this@mockk.accounts } returns listOf(accountDTO) + } + + accountsResponseStoreFlow.value = response + + val exception = Exception("Test error") + + every { cryptoPortfolioConverter.convert(accountDTO) } throws exception + + // Act + val actual = runCatching { repository.getAccountSync(accountId) }.exceptionOrNull()!! + + // Assert + val expected = exception + Truth.assertThat(actual).isSameInstanceAs(expected) + Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) + + verifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + convertersContainer.createCryptoPortfolioConverter(userWalletId) + cryptoPortfolioConverter.convert(accountDTO) + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetArchivedAccountSync { + + private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main) + + @Test + fun `getArchivedAccount should return None when archived accounts are null`() = runTest { + // Arrange + archivedAccountsInnerStore.store(value = null) + + // Act + val actual = repository.getArchivedAccountSync(accountId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.getSyncOrNull() + } + } + + @Test + fun `getArchivedAccount should return None when archived account not found`() = runTest { + // Arrange + archivedAccountsInnerStore.store(value = listOf()) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds) + + // Act + val actual = repository.getArchivedAccountSync(accountId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.getSyncOrNull() + } + } + + @Test + fun `getArchivedAccount should return ArchivedAccount when found`() = runTest { + // Arrange + val archivedAccount = ArchivedAccount( + accountId = accountId, + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = DerivationIndex.Main, + tokensCount = 0, + networksCount = 0, + ) + + archivedAccountsInnerStore.store(value = listOf(archivedAccount)) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds) + + // Act + val actual = repository.getArchivedAccountSync(accountId) + + // Assert + val expected = archivedAccount.toOption() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.getSyncOrNull() + } + } + + @Test + fun `getArchivedAccount should throws exception when store throws exception`() = runTest { + // Arrange + val exception = Exception("Test error") + + coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception + + // Act + val actual = runCatching { repository.getArchivedAccountSync(accountId) }.exceptionOrNull()!! + + // Assert + val expected = exception + Truth.assertThat(actual).isSameInstanceAs(expected) + Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) + + coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetArchivedAccountListSync { + + @Test + fun `getArchivedAccountListSync should return None when archived accounts are null`() = runTest { + // Arrange + archivedAccountsInnerStore.store(value = null) + + // Act + val actual = repository.getArchivedAccountListSync(userWalletId) + + // Assert + Truth.assertThat(actual).isEqualTo(None) + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.getSyncOrNull() + } + } + + @Test + fun `getArchivedAccountListSync should return Option with list when archived accounts exist`() = runTest { + // Arrange + val archivedAccount1 = mockk() + val archivedAccount2 = mockk() + val archivedAccounts = listOf(archivedAccount1, archivedAccount2) + archivedAccountsInnerStore.store(value = archivedAccounts) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds) + + // Act + val actual = repository.getArchivedAccountListSync(userWalletId) + + // Assert + val expected = archivedAccounts.toOption() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.getSyncOrNull() + } + } + + @Test + fun `getArchivedAccountListSync should throw exception when store throws exception`() = runTest { + // Arrange + val exception = Exception("Test error") + coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception + + // Act + val actual = runCatching { repository.getArchivedAccountListSync(userWalletId) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isSameInstanceAs(exception) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetArchivedAccounts { + + @Test + fun `getArchivedAccounts should emit empty list when no archived accounts`() = runTest { + // Arrange + archivedAccountsInnerStore.store(value = null) + + val archivedAccountsFlow = repository.getArchivedAccounts(userWalletId) + + // Act + val actual = getEmittedValues(archivedAccountsFlow) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.get() + } + } + + @Test + fun `getArchivedAccounts should emit list of archived accounts when present`() = runTest { + // Arrange + val archivedAccount1 = mockk() + val archivedAccount2 = mockk() + val archivedAccounts = listOf(archivedAccount1, archivedAccount2) + + archivedAccountsInnerStore.store(value = archivedAccounts) + archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds) + + val archivedAccountsFlow = repository.getArchivedAccounts(userWalletId) + + // Act + val actual = getEmittedValues(archivedAccountsFlow) + + // Assert + val expected = listOf(archivedAccounts) + Truth.assertThat(actual).containsExactlyElementsIn(expected) + coVerifyOrder { + archivedAccountsStoreFactory.create(userWalletId) + archivedAccountsStore.get() + } + } + + @Test + fun `getArchivedAccounts should throw exception when store throws exception`() = runTest { + // Arrange + val exception = Exception("Test error") + coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception + + // Act + val actual = runCatching { repository.getArchivedAccounts(userWalletId) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isSameInstanceAs(exception) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class FetchArchivedAccounts { + + private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main) + + @Test + fun `fetchArchivedAccounts should store archived accounts in store`() = runTest { + // Arrange + val accountDTO = WalletAccountDTO( + id = accountId.value, + name = "Archived Account", + derivationIndex = 0, + icon = CryptoPortfolioIcon.Icon.Wallet.name, + iconColor = CryptoPortfolioIcon.Color.DullLavender.name, + totalNetworks = 0, + totalTokens = 0, + ) + + val apiResponse = mockk { + every { this@mockk.accounts } returns listOf(accountDTO) + } + + val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO) + + coEvery { + tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) + } returns ApiResponse.Success(apiResponse) + + // Act + repository.fetchArchivedAccounts(userWalletId) + val actual = archivedAccountsStore.getSyncOrNull() + + // Assert + Truth.assertThat(actual).containsExactly(archivedAccount) + + coVerifyOrder { + tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) + archivedAccountsStoreFactory.create(userWalletId) + } + } + + @Test + fun `fetchArchivedAccounts should throw exception if API returns error`() = runTest { // Arrange + val exception = Exception("API error") + coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) } throws exception + + // Act + val actual = runCatching { repository.fetchArchivedAccounts(userWalletId) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isSameInstanceAs(exception) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull() + + coVerify { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SaveAccounts { + + private val version = 1 + + @Test + fun `saveAccounts should call API and update store`() = runTest { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + } + + val accountList = AccountList.empty(userWallet = userWallet) + + val accountsResponse = mockk { + every { this@mockk.wallet.version } returns version + } + + accountsResponseStoreFlow.value = accountsResponse + + val body = SaveWalletAccountsResponseConverter.convert(value = accountList) + + val apiResponse = ApiResponse.Success(Unit) + + coEvery { + tangemTechApi.saveWalletAccounts( + walletId = userWalletId.stringValue, + ifMatch = version.toString(), + body = body, + ) + } returns apiResponse + + val converter = mockk { + every { this@mockk.convert(accountList) } returns accountsResponse + } + + every { + convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet, version = version) + } returns converter + + coEvery { accountsResponseStore.updateData(transform = any()) } returns accountsResponse + + // Act + repository.saveAccounts(accountList) + + // Assert + Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse) + + coVerifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body) + convertersContainer.getWalletAccountsResponseCF.create(userWallet, version) + converter.convert(accountList) + accountsResponseStore.updateData(any()) + } + } + + @Test + fun `saveAccounts if API request is failed`() = runTest { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + } + + val accountList = AccountList.empty(userWallet = userWallet) + + val accountsResponse = mockk { + every { this@mockk.wallet.version } returns version + } + + accountsResponseStoreFlow.value = accountsResponse + + val body = SaveWalletAccountsResponseConverter.convert(value = accountList) + + val apiResponse = ApiResponse.Error(cause = ApiResponseError.NetworkException) as ApiResponse + + coEvery { + tangemTechApi.saveWalletAccounts( + walletId = userWalletId.stringValue, + ifMatch = version.toString(), + body = body, + ) + } returns apiResponse + + // Act + val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isEqualTo(ApiResponseError.NetworkException) + + coVerifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body) + } + + coVerify(inverse = true) { + convertersContainer.getWalletAccountsResponseCF.create(any(), any()) + accountsResponseStore.updateData(any()) + } + } + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt index 977c3c4169..a7358ba43d 100644 --- a/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/store/ArchivedAccountsStoreFactoryTest.kt @@ -9,7 +9,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ArchivedAccountsStoreFactoryTest { - private val factory = ArchivedAccountsStoreFactory() + private val factory = ArchivedAccountsStoreFactory @AfterEach fun tearDownEach() { diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index ac6921e167..8904840bcb 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -22,7 +22,7 @@ interface AccountsCRUDRepository { * @param userWalletId the unique identifier of the user wallet * @return an [Option] containing the [AccountList] if found, or `Option.None` if not */ - suspend fun getAccounts(userWalletId: UserWalletId): Option + suspend fun getAccountListSync(userWalletId: UserWalletId): Option /** * Retrieves a specific account by its unique identifier @@ -30,14 +30,14 @@ interface AccountsCRUDRepository { * @param accountId the unique identifier of the account * @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not */ - suspend fun getAccount(accountId: AccountId): Option + suspend fun getAccountSync(accountId: AccountId): Option /** * Retrieves a archived account by its unique identifier * * @param accountId the unique identifier of the account */ - suspend fun getArchivedAccount(accountId: AccountId): Option + suspend fun getArchivedAccountSync(accountId: AccountId): Option /** * Retrieves a list of archived accounts associated with a specific user wallet @@ -45,7 +45,7 @@ interface AccountsCRUDRepository { * @param userWalletId the unique identifier of the user wallet * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not */ - suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + suspend fun getArchivedAccountListSync(userWalletId: UserWalletId): Option> /** * Provides a flow of archived accounts associated with a specific user wallet @@ -73,7 +73,7 @@ interface AccountsCRUDRepository { * * @param userWalletId the unique identifier of the user wallet */ - suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int + suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option /** * Retrieves a user wallet by its unique identifier diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 6a3866bc4b..6f233d2da7 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -55,7 +55,7 @@ class AddCryptoPortfolioUseCase( newAccount } - private fun Raise.createAccount( + private fun createAccount( userWalletId: UserWalletId, accountName: AccountName, icon: CryptoPortfolioIcon, @@ -72,7 +72,7 @@ class AddCryptoPortfolioUseCase( private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { return catch( - block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt index 0611b106fb..75056d6eb0 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -40,7 +40,7 @@ class ArchiveCryptoPortfolioUseCase( private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( - block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt index cbcfb13168..afaa2a587e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -57,7 +57,7 @@ class GetArchivedAccountsUseCase( private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { return Either.catch { - crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + crudRepository.getArchivedAccountListSync(userWalletId = userWalletId).getOrElse { error("Archived accounts not found for user wallet: $userWalletId") } } @@ -70,7 +70,11 @@ class GetArchivedAccountsUseCase( private suspend fun ProducerScope>.subscribeOnArchivedAccounts( userWalletId: UserWalletId, ) { - crudRepository.getArchivedAccounts(userWalletId) + runCatching { crudRepository.getArchivedAccounts(userWalletId) } + .getOrElse { + send(it.lceError()) + return + } .distinctUntilChanged() .retryWhen { cause, _ -> send(cause.lceError()) diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt index c34240e22b..f511ab03b8 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -38,6 +38,7 @@ class GetUnoccupiedAccountIndexUseCase( block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) + .getOrElse { raise(Error.DataNotFound) } } /** @@ -48,6 +49,10 @@ class GetUnoccupiedAccountIndexUseCase( val tag: String get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error" + data object DataNotFound : Error { + override fun toString(): String = "$tag: Data not found" + } + /** Error indicating that the derivation index is invalid */ data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error { override fun toString(): String = "$tag: Invalid derivation index: $cause" diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index f5dcec41aa..1bff615daf 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -44,7 +44,7 @@ class RecoverCryptoPortfolioUseCase( private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( - block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } @@ -52,7 +52,7 @@ class RecoverCryptoPortfolioUseCase( private suspend fun Raise.getArchivedAccount(accountId: AccountId): ArchivedAccount { return catch( - block = { crudRepository.getArchivedAccount(accountId = accountId) }, + block = { crudRepository.getArchivedAccountSync(accountId = accountId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) .getOrElse { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index 4451a5f50d..2145e7ed15 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -61,7 +61,7 @@ class UpdateCryptoPortfolioUseCase( private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( - block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index f30bc1dfda..f677187f17 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -41,7 +41,7 @@ class AddCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() // Act val actual = useCase( @@ -56,7 +56,7 @@ class AddCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) + crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) } @@ -69,7 +69,7 @@ class AddCryptoPortfolioUseCaseTest { val newAccount = createNewAccount() val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId) } returns None + coEvery { crudRepository.getAccountListSync(userWalletId) } returns None coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet // Act @@ -85,7 +85,7 @@ class AddCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) + crudRepository.getAccountListSync(userWalletId) crudRepository.getUserWallet(userWalletId) crudRepository.saveAccounts(newAccountList) } @@ -102,7 +102,7 @@ class AddCryptoPortfolioUseCaseTest { val newAccount = createNewAccount(derivationIndex = 21) - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() // Act val actual = useCase( @@ -119,7 +119,7 @@ class AddCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getUserWallet(any()) @@ -133,7 +133,7 @@ class AddCryptoPortfolioUseCaseTest { val newAccount = createNewAccount() val exception = IllegalStateException("Test error") - coEvery { crudRepository.getAccounts(userWalletId) } throws exception + coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act val actual = useCase( @@ -147,7 +147,7 @@ class AddCryptoPortfolioUseCaseTest { val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getUserWallet(any()) @@ -164,7 +164,7 @@ class AddCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Test error") - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception // Act @@ -180,7 +180,7 @@ class AddCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) + crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index 1e442938fa..fe67edc618 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -41,7 +41,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList - account).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId) @@ -51,7 +51,7 @@ class ArchiveCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) + crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) } } @@ -64,7 +64,7 @@ class ArchiveCryptoPortfolioUseCaseTest { derivationIndex = DerivationIndex.Main, ) - coEvery { crudRepository.getAccounts(userWalletId) } returns None + coEvery { crudRepository.getAccountListSync(userWalletId) } returns None // Act val actual = useCase(accountId) @@ -73,7 +73,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(any()) } } @@ -87,7 +87,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Test error") - coEvery { crudRepository.getAccounts(userWalletId) } throws exception + coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act val actual = useCase(accountId) @@ -96,7 +96,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(any()) } } @@ -109,7 +109,7 @@ class ArchiveCryptoPortfolioUseCaseTest { derivationIndex = DerivationIndex(1).getOrNull()!!, ) - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId) @@ -118,7 +118,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val expected = Error.CriticalTechError.AccountNotFound(accountId).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(any()) } } @@ -133,7 +133,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Save failed") - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception // Act @@ -144,7 +144,7 @@ class ArchiveCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) + crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) } } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt index eb0019f93c..5fd7b2ccce 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -43,7 +43,7 @@ class GetArchivedAccountsUseCaseTest { mockk(), mockk(), ) - coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns archivedAccounts.toOption() every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) // Act @@ -54,7 +54,7 @@ class GetArchivedAccountsUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccountListSync(userWalletId) crudRepository.getArchivedAccounts(userWalletId) } @@ -69,7 +69,7 @@ class GetArchivedAccountsUseCaseTest { mockk(), ) - coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns None every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) // Act @@ -83,7 +83,7 @@ class GetArchivedAccountsUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerify(exactly = 1) { - crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccountListSync(userWalletId) crudRepository.fetchArchivedAccounts(userWalletId) crudRepository.getArchivedAccounts(userWalletId) } @@ -98,7 +98,7 @@ class GetArchivedAccountsUseCaseTest { mockk(), ) - coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } throws exception every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) // Act @@ -112,7 +112,7 @@ class GetArchivedAccountsUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerify(exactly = 1) { - crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccountListSync(userWalletId) crudRepository.fetchArchivedAccounts(userWalletId) crudRepository.getArchivedAccounts(userWalletId) } @@ -123,7 +123,7 @@ class GetArchivedAccountsUseCaseTest { // Arrange val exception = IllegalStateException("Fetch error") - coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + coEvery { crudRepository.getArchivedAccountListSync(userWalletId) } returns None every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception @@ -139,7 +139,7 @@ class GetArchivedAccountsUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerify(exactly = 1) { - crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccountListSync(userWalletId) crudRepository.fetchArchivedAccounts(userWalletId) crudRepository.getArchivedAccounts(userWalletId) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt index 4c994aeb21..a0ba98b709 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -1,6 +1,7 @@ package com.tangem.domain.account.usecase import arrow.core.left +import arrow.core.toOption import com.google.common.truth.Truth import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.DerivationIndex @@ -29,7 +30,7 @@ class GetUnoccupiedAccountIndexUseCaseTest { @Test fun `invoke should return next unoccupied index when repository returns count`() = runTest { // Arrange - coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3 + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3.toOption() // Act val actual = useCase(userWalletId = userWalletId) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 7c8f1a847f..eb4e423c11 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -52,8 +52,8 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() // Act val actual = useCase(account.accountId) @@ -63,8 +63,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) - crudRepository.getArchivedAccount(account.accountId) + crudRepository.getAccountListSync(userWalletId) + crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) } } @@ -77,7 +77,7 @@ class RecoverCryptoPortfolioUseCaseTest { derivationIndex = DerivationIndex.Main, ) - coEvery { crudRepository.getAccounts(userWalletId) } returns None + coEvery { crudRepository.getAccountListSync(userWalletId) } returns None // Act val actual = useCase(accountId) @@ -86,9 +86,9 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { - crudRepository.getArchivedAccount(any()) + crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) } } @@ -102,7 +102,7 @@ class RecoverCryptoPortfolioUseCaseTest { ) val exception = IllegalStateException("Test error") - coEvery { crudRepository.getAccounts(userWalletId) } throws exception + coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act val actual = useCase(accountId) @@ -111,9 +111,9 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { - crudRepository.getArchivedAccount(any()) + crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) } } @@ -125,8 +125,8 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val exception = IllegalStateException("Test error") - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccountSync(account.accountId) } throws exception // Act val actual = useCase(account.accountId) @@ -136,8 +136,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) - crudRepository.getArchivedAccount(account.accountId) + crudRepository.getAccountListSync(userWalletId) + crudRepository.getArchivedAccountSync(account.accountId) } coVerify(inverse = true) { crudRepository.saveAccounts(any()) } } @@ -148,8 +148,8 @@ class RecoverCryptoPortfolioUseCaseTest { val account = createAccount(userWalletId) val accountList = AccountList.empty(userWallet) - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None // Act val actual = useCase(account.accountId) @@ -159,8 +159,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) - crudRepository.getArchivedAccount(account.accountId) + crudRepository.getAccountListSync(userWalletId) + crudRepository.getArchivedAccountSync(account.accountId) } coVerify(inverse = true) { crudRepository.saveAccounts(any()) } } @@ -182,8 +182,8 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") - coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception // Act @@ -194,8 +194,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId) - crudRepository.getArchivedAccount(account.accountId) + crudRepository.getAccountListSync(userWalletId) + crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) } } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index f1ba896a7c..5d6625fbe0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -48,7 +48,7 @@ class UpdateCryptoPortfolioUseCaseTest { val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId = accountId, accountName = newAccountName) @@ -58,7 +58,7 @@ class UpdateCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.getAccountListSync(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) } } @@ -76,7 +76,7 @@ class UpdateCryptoPortfolioUseCaseTest { val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId = accountId, icon = newAccountIcon) @@ -86,7 +86,7 @@ class UpdateCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.getAccountListSync(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) } } @@ -105,7 +105,7 @@ class UpdateCryptoPortfolioUseCaseTest { val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon) @@ -115,7 +115,7 @@ class UpdateCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.getAccountListSync(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) } } @@ -126,7 +126,7 @@ class UpdateCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId = accountId) @@ -136,7 +136,7 @@ class UpdateCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerify(inverse = true) { - crudRepository.getAccounts(userWalletId = any()) + crudRepository.getAccountListSync(userWalletId = any()) crudRepository.saveAccounts(accountList = any()) } } @@ -151,7 +151,7 @@ class UpdateCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Test exception") - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } throws exception // Act val actual = useCase(accountId = accountId, accountName = newAccountName) @@ -160,7 +160,7 @@ class UpdateCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(cause = exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } } @@ -175,7 +175,7 @@ class UpdateCryptoPortfolioUseCaseTest { val newAccountName = AccountName("New name").getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList // Act val actual = useCase(accountId = accountId, accountName = newAccountName) @@ -184,7 +184,7 @@ class UpdateCryptoPortfolioUseCaseTest { val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } } @@ -199,7 +199,7 @@ class UpdateCryptoPortfolioUseCaseTest { val newAccountName = AccountName("New name").getOrNull()!! - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act val actual = useCase(accountId = accountId, accountName = newAccountName) @@ -208,7 +208,7 @@ class UpdateCryptoPortfolioUseCaseTest { val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } } @@ -224,7 +224,7 @@ class UpdateCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Save failed") - coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception // Act @@ -235,7 +235,7 @@ class UpdateCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.getAccountListSync(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) } } From 31558ee15d409ce1b242f67ded73cf2353a95882 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 11:32:46 +0000 Subject: [PATCH 22/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 57e50cf4ac..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1148" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 2836d7bb2bc1a369fc8c9816d74d869e3a7a9a39 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 14:38:10 +0300 Subject: [PATCH 23/48] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 16 ++++++++++++++++ .../feature/impl/DevFeatureTogglesManager.kt | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 823d7df27c..7fd98efe81 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -14,6 +14,8 @@ import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.rules.ApiEnvironmentRule +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -46,6 +48,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var appPreferencesStore: AppPreferencesStore + @Inject + lateinit var featureTogglesManager: FeatureTogglesManager + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -90,6 +95,7 @@ abstract class BaseTestCase : TestCase( apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) Intents.init() + setFeatureToggles() additionalBeforeSection() }.after { additionalAfterSection() @@ -113,4 +119,14 @@ abstract class BaseTestCase : TestCase( { composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth) } + + + private fun setFeatureToggles() { + runBlocking { + with(featureTogglesManager as MutableFeatureTogglesManager) { + changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true) + changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true) + } + } + } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 6facf5dc8b..6677cae6d0 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -11,7 +11,7 @@ import java.util.Locale import kotlin.properties.Delegates /** - * Feature toggles manager implementation in DEV build + * Feature toggles manager implementation in dev or mocked build * * @property versionProvider application version provider * @property featureTogglesLocalStorage local storage for feature toggles From bc7d7ea7b6fdb80a6f8d400b766b7c3585fc0df2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Aug 2025 15:41:41 +0200 Subject: [PATCH 24/48] Updated on 2026-08-14 --- .gitignore | 4 + app/build.gradle.kts | 33 ++++++ app/proguard-rules.pro | 7 ++ .../FirebasePushNotificationsTokenProvider.kt | 2 +- .../com/tangem/tap/di/GooglePushModule.kt} | 6 +- app/src/huawei/AndroidManifest.xml | 17 +++ .../HuaweiPushNotificationsTokenProvider.kt | 50 +++++++++ .../java/com/tangem/tap/HuaweiPushService.kt | 47 ++++++++ .../com/tangem/tap/di/HuaweiPushModule.kt | 18 ++++ .../AppConfigurationProviderImpl.kt | 13 +++ .../common/pushes/PushNotificationDelegate.kt | 102 ++++++++++++++++++ .../pushes/TangemPushNotificationService.kt | 92 +++------------- .../tangem/tap/di/AppConfigurationModule.kt | 18 ++++ build.gradle.kts | 7 ++ .../buildConfig/AppConfigurationProvider.kt | 6 ++ gradle/dependencies.toml | 16 +++ .../extension/AppExtensionConfigurations.kt | 1 + settings.gradle.kts | 2 + 18 files changed, 357 insertions(+), 84 deletions(-) rename app/src/{main/java/com/tangem/tap/data => google/java/com/tangem/tap}/FirebasePushNotificationsTokenProvider.kt (95%) rename app/src/{main/java/com/tangem/tap/di/data/PushNotificationsModule.kt => google/java/com/tangem/tap/di/GooglePushModule.kt} (74%) create mode 100644 app/src/huawei/AndroidManifest.xml create mode 100644 app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt create mode 100644 app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt create mode 100644 app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt create mode 100644 app/src/main/java/com/tangem/tap/common/buildconfig/AppConfigurationProviderImpl.kt create mode 100644 app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt create mode 100644 app/src/main/java/com/tangem/tap/di/AppConfigurationModule.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/buildConfig/AppConfigurationProvider.kt diff --git a/.gitignore b/.gitignore index 6106311a37..0144ddc6e7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ local.properties # Google services + +# Huawei services +app/agconnect-services.json + app/src/debug/google-services.json app/src/internal/google-services.json app/src/external/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 17ee83e2e2..720052668f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,9 +11,14 @@ plugins { alias(deps.plugins.firebase.crashlytics) alias(deps.plugins.firebase.perf) alias(deps.plugins.ksp) + id(deps.plugins.agconnect.get().pluginId) id("configuration") } +agcp { + manifest = false +} + android { namespace = "com.tangem.wallet" testOptions { @@ -53,12 +58,35 @@ android { keyPassword = keystoreProperties["key_password"] as String } } + + flavorDimensions += "services" + + productFlavors { + create("google") { + dimension = "services" + buildConfigField("String", "FLAVOR_NAME", "\"google\"") + } + create("huawei") { + dimension = "services" + buildConfigField("String", "FLAVOR_NAME", "\"huawei\"") + } + } + + buildTypes { + debug { + buildConfigField("String", "BUILD_TYPE", "\"debug\"") + } + release { + buildConfigField("String", "BUILD_TYPE", "\"release\"") + } + } } configurations.all { exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") exclude(group = "com.github.komputing.kethereum") + exclude(group = "com.android.tools.build", module = "gradle") resolutionStrategy { dependencySubstitution { @@ -379,4 +407,9 @@ dependencies { // excludes version 9999.0-empty-to-avoid-conflict-with-guava exclude(group = "com.google.guava", module = "listenablefuture") } + + /** Huawei flavor-specific dependencies */ + "huaweiImplementation"(deps.huawei.push) + "huaweiImplementation"(deps.agconnect.agcp) + "huaweiImplementation"(deps.agconnect.core) } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2a7b205c9d..4e36894e88 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,6 +7,13 @@ -keep class com.google.android.gms.internal.** { *; } -keepclasseswithmembers class com.google.firebase.FirebaseException +# huawei push kit +-ignorewarnings +-keepattributes SourceFile,LineNumberTable +-keep class com.huawei.hianalytics.**{*;} +-keep class com.huawei.updatesdk.**{*;} +-keep class com.huawei.hms.**{*;} + # hedera sdk -keep class com.hedera.hashgraph.sdk.** { *; } -keep interface com.hedera.hashgraph.sdk.** { *; } diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt rename to app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt index 358bdd3d61..627e41f2cc 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.data +package com.tangem.tap import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider diff --git a/app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt b/app/src/google/java/com/tangem/tap/di/GooglePushModule.kt similarity index 74% rename from app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt rename to app/src/google/java/com/tangem/tap/di/GooglePushModule.kt index 063efaf3dc..a8e4e87c08 100644 --- a/app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt +++ b/app/src/google/java/com/tangem/tap/di/GooglePushModule.kt @@ -1,6 +1,6 @@ -package com.tangem.tap.di.data +package com.tangem.tap.di -import com.tangem.tap.data.FirebasePushNotificationsTokenProvider +import com.tangem.tap.FirebasePushNotificationsTokenProvider import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.Binds import dagger.Module @@ -10,7 +10,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal interface PushNotificationsModule { +internal interface GooglePushModule { @Binds @Singleton diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml new file mode 100644 index 0000000000..0c769dbe3e --- /dev/null +++ b/app/src/huawei/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt new file mode 100644 index 0000000000..03c709cd6c --- /dev/null +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt @@ -0,0 +1,50 @@ +package com.tangem.tap + +import android.content.Context +import com.google.firebase.messaging.FirebaseMessaging +import com.huawei.agconnect.AGConnectOptionsBuilder +import com.huawei.hms.aaid.HmsInstanceId +import com.huawei.hms.common.ApiException +import com.tangem.google.GoogleServicesHelper +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.notifications.PushNotificationsTokenProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +internal class HuaweiPushNotificationsTokenProvider @Inject constructor( + @ApplicationContext private val context: Context, + private val coroutineDispatcherProvider: CoroutineDispatcherProvider, +) : PushNotificationsTokenProvider { + + override suspend fun getToken(): String { + val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(context) + return if (isGoogleServicesAvailable) { + try { + FirebaseMessaging.getInstance().token.await() + } catch (ex: Exception) { + Timber.e(ex) + "" + } + } else { + withContext(coroutineDispatcherProvider.io) { + try { + val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY) + val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE) + Timber.i("Requested token from HuaweiService: $token") + token + } catch (e: ApiException) { + Timber.i("Fetching token from HuaweiService failed cause: ${e.message}") + "" + } + } + } + } + + companion object { + private const val APP_ID_KEY = "client/app_id" + private const val TOKEN_REQUEST_MODE = "HCM" + } +} \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt new file mode 100644 index 0000000000..b7a1de528d --- /dev/null +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt @@ -0,0 +1,47 @@ +package com.tangem.tap + +import android.os.Bundle +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage +import com.tangem.google.GoogleServicesHelper +import com.tangem.tap.common.pushes.PushNotificationDelegate +import timber.log.Timber + +class HuaweiPushService : HmsMessageService() { + + private val pushNotificationDelegate: PushNotificationDelegate by lazy { + PushNotificationDelegate(applicationContext) + } + + override fun onNewToken(token: String?, bundle: Bundle?) { + super.onNewToken(token, bundle) + Timber.i("HuaweiPushService: On new token from HuaweiService: $token") + } + + override fun onTokenError(e: Exception?, bundle: Bundle?) { + super.onTokenError(e, bundle) + Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}") + } + + override fun onMessageReceived(message: RemoteMessage?) { + super.onMessageReceived(message) + val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this) + if (isGoogleServicesAvailable) return + val notification = message?.notification ?: return + val channelId = notification.channelId ?: TANGEM_CHANNEL_ID + + pushNotificationDelegate.showNotification( + dataMap = message.dataOfMap, + title = notification.title, + body = notification.body, + channelId = channelId, + priority = message.urgency, + imageUrl = notification.imageUrl, + vibratePattern = notification.vibrateConfig, + ) + } + + private companion object { + const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications + } +} \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt b/app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt new file mode 100644 index 0000000000..f4379c5469 --- /dev/null +++ b/app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di + +import com.tangem.tap.HuaweiPushNotificationsTokenProvider +import com.tangem.utils.notifications.PushNotificationsTokenProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface HuaweiPushModule { + + @Binds + @Singleton + fun bindPushNotificationsTokenProvider(impl: HuaweiPushNotificationsTokenProvider): PushNotificationsTokenProvider +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/buildconfig/AppConfigurationProviderImpl.kt b/app/src/main/java/com/tangem/tap/common/buildconfig/AppConfigurationProviderImpl.kt new file mode 100644 index 0000000000..7a87d69e7c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/buildconfig/AppConfigurationProviderImpl.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common.buildconfig + +import com.tangem.utils.buildConfig.AppConfigurationProvider +import com.tangem.wallet.BuildConfig +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class AppConfigurationProviderImpl @Inject constructor() : AppConfigurationProvider { + + override fun isDebug(): Boolean = BuildConfig.BUILD_TYPE == "debug" + override fun isHuawei(): Boolean = BuildConfig.FLAVOR_NAME == "huawei" +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt b/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt new file mode 100644 index 0000000000..844eec86ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/pushes/PushNotificationDelegate.kt @@ -0,0 +1,102 @@ +package com.tangem.tap.common.pushes + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import coil.executeBlocking +import coil.request.ImageRequest +import com.tangem.domain.common.LogConfig +import com.tangem.tap.MainActivity +import com.tangem.tap.common.images.createCoilImageLoader +import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler +import com.tangem.wallet.R + +class PushNotificationDelegate(private val context: Context) { + + @Suppress("LongParameterList") + fun showNotification( + dataMap: Map, + title: String?, + body: String?, + channelId: String, + priority: Int, + imageUrl: Uri? = null, + vibratePattern: LongArray?, + ) { + val intent = Intent(context, MainActivity::class.java).apply { + dataMap.forEach { (key, value) -> + putExtra(key, value) + } + putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + val pendingIntent = PendingIntent.getActivity( + /* context = */ context, + /* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE, + /* intent = */ intent, + /* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_tangem_24) + .setContentTitle(title) + .setContentText(body) + .setPriority(priority) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setVibrate(vibratePattern) + .apply { + imageUrl?.let { uri -> + val bitmap = getBitmapImageFromUrl(uri) + setStyle( + NotificationCompat + .BigPictureStyle() + .bigPicture(bitmap), + ).setLargeIcon(bitmap) + } + } + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel( + channelId, + ContextCompat.getString(context, R.string.tangem_app_name), + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(notificationChannel) + } + + // Generating unique notification id + val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt() + + notificationManager.notify( + /* id = */ uniqueId, + /* notification = */ notificationBuilder.build(), + ) + } + + private fun getBitmapImageFromUrl(url: Uri): Bitmap? { + return createCoilImageLoader( + context, + logEnabled = LogConfig.imageLoader, + ).executeBlocking( + ImageRequest.Builder(context) + .data(url) + .build(), + ).drawable?.toBitmap() + } + + private companion object { + const val PUSH_NOTIFICATION_REQUEST_CODE = 123 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index ebeec6dffc..97026725ea 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -1,30 +1,17 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import coil.executeBlocking -import coil.request.ImageRequest import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -import com.tangem.domain.common.LogConfig -import com.tangem.tap.MainActivity -import com.tangem.tap.common.images.createCoilImageLoader -import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler -import com.tangem.wallet.R import timber.log.Timber @SuppressLint("MissingFirebaseInstanceTokenRefresh") internal class TangemPushNotificationService : FirebaseMessagingService() { + private val pushNotificationDelegate: PushNotificationDelegate by lazy { + PushNotificationDelegate(applicationContext) + } + override fun onNewToken(token: String) { super.onNewToken(token) Timber.d("New FCM token received: $token") @@ -36,73 +23,18 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID - val intent = Intent(applicationContext, MainActivity::class.java).apply { - message.data.forEach { - putExtra(it.key, it.value) - } - putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } - val pendingIntent = PendingIntent.getActivity( - /* context = */ this, - /* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE, - /* intent = */ intent, - /* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + pushNotificationDelegate.showNotification( + dataMap = message.data, + title = notification.title, + body = notification.body, + channelId = channelId, + priority = message.priority, + imageUrl = notification.imageUrl, + vibratePattern = notification.vibrateTimings, ) - - val notificationBuilder = - NotificationCompat.Builder(applicationContext, channelId) - .setSmallIcon(R.drawable.ic_tangem_24) - .setContentTitle(notification.title) - .setContentText(notification.body) - .setPriority(message.priority) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - .setVibrate(notification.vibrateTimings) - .apply { - notification.imageUrl?.let { uri -> - val bitmap = getBitmapImageFromUrl(uri) - setStyle( - NotificationCompat - .BigPictureStyle() - .bigPicture(bitmap), - ).setLargeIcon(bitmap) - } - } - - val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationChannel = NotificationChannel( - channelId, - ContextCompat.getString(applicationContext, R.string.tangem_app_name), - NotificationManager.IMPORTANCE_HIGH, - ) - notificationManager.createNotificationChannel(notificationChannel) - } - - // Generating unique notification id - val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt() - - notificationManager.notify( - /* id = */ uniqueId, - /* notification = */ notificationBuilder.build(), - ) - } - - private fun getBitmapImageFromUrl(url: Uri): Bitmap? { - return createCoilImageLoader( - applicationContext, - logEnabled = LogConfig.imageLoader, - ).executeBlocking( - ImageRequest.Builder(applicationContext) - .data(url) - .build(), - ).drawable?.toBitmap() } private companion object { const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications - const val PUSH_NOTIFICATION_REQUEST_CODE = 123 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppConfigurationModule.kt b/app/src/main/java/com/tangem/tap/di/AppConfigurationModule.kt new file mode 100644 index 0000000000..42148c10d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/AppConfigurationModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di + +import com.tangem.tap.common.buildconfig.AppConfigurationProviderImpl +import com.tangem.utils.buildConfig.AppConfigurationProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AppConfigurationModule { + + @Binds + @Singleton + fun bindAppConfigurationProvider(impl: AppConfigurationProviderImpl): AppConfigurationProvider +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c490aef8c2..5d73b767fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,13 @@ plugins { alias(deps.plugins.ksp) apply false } +buildscript { + dependencies { + classpath(deps.gradle.android) + classpath(deps.agconnect.agcp) + } +} + val clean by tasks.registering { delete(rootProject.buildDir) } diff --git a/core/utils/src/main/java/com/tangem/utils/buildConfig/AppConfigurationProvider.kt b/core/utils/src/main/java/com/tangem/utils/buildConfig/AppConfigurationProvider.kt new file mode 100644 index 0000000000..863864842e --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/buildConfig/AppConfigurationProvider.kt @@ -0,0 +1,6 @@ +package com.tangem.utils.buildConfig + +interface AppConfigurationProvider { + fun isDebug(): Boolean + fun isHuawei(): Boolean +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f9ef6078d0..37e38fce69 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -11,6 +11,12 @@ ksp = "2.1.10-1.0.30" firebasePerf = "1.4.2" # endregion Classpath +# region AppGallery +agconnect = "1.9.1.304" +huaweiServices = "6.9.0.301" +huaweiPush = "6.11.0.300" +# endregion AppGallery + # region AndroidX androidxActivityCompose = "1.8.0" androidxAppCompat = "1.5.1" @@ -130,6 +136,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } room = { id = "androidx.room", version.ref = "room" } kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +agconnect = { id = "com.huawei.agconnect", version.ref = "agconnect" } [libraries] # region Classpath @@ -190,6 +197,15 @@ firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" } firebase-perf = { module = "com.google.firebase:firebase-perf" } # endregion Firebase +# region AppGallery +agconnect-agcp = { module = "com.huawei.agconnect:agcp", version.ref = "agconnect" } +agconnect-core = { module = "com.huawei.agconnect:agconnect-core", version.ref = "agconnect" } +agconnect-crash = { module = "com.huawei.agconnect:agconnect-crash", version.ref = "agconnect" } +huawei-base = { module = "com.huawei.hms:base", version.ref = "huaweiServices" } +huawei-analytics = { module = "com.huawei.hms:hianalytics", version.ref = "huaweiServices" } +huawei-push = { module = "com.huawei.hms:push", version.ref = "huaweiPush" } +# endregion AppGallery + # region Detekt detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 6f03ce1a3a..88e9d25480 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -75,6 +75,7 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b } BuildType.Debug -> { isDebuggable = true + signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } BuildType.Internal, BuildType.External diff --git a/settings.gradle.kts b/settings.gradle.kts index 7c7251cb46..1511ce8a21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() + maven { url = uri("https://developer.huawei.com/repo/") } } includeBuild("plugins/configuration") @@ -35,6 +36,7 @@ dependencyResolutionManagement { } } mavenCentral() + maven { url = uri("https://developer.huawei.com/repo/") } mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") From 4c2ce375e919a26b942a0a46d47fc6ac7eed1967 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 08:53:06 +0200 Subject: [PATCH 25/48] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 ++++ .../appbar/models/TopAppBarButtonUM.kt | 6 ++++++ .../onramp/main/entity/OnrampIntents.kt | 1 + .../main/model/OnrampMainComponentModel.kt | 2 ++ .../DefaultOnrampNewMainFeatureToggle.kt | 10 +++++++++ .../onramp/newmain/OnrampNewMainComponent.kt | 21 +++++++++++++++++++ .../newmain/OnrampNewMainFeatureToggle.kt | 5 +++++ 7 files changed, 49 insertions(+) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 318f39ce65..9d99dceed5 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -66,5 +66,9 @@ { "name": "YIELD_LENDING_FEATURE_ENABLED", "version": "undefined" + }, + { + "name": "NEW_ONRAMP_MAIN_ENABLED", + "version": "undefined" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt index aa06b614a0..3d1549e138 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt @@ -32,6 +32,12 @@ sealed class TopAppBarButtonUM( enabled = enabled, ) + fun Close(enabled: Boolean = true, onCloseClick: () -> Unit) = Icon( + iconRes = R.drawable.ic_close_24, + onClicked = onCloseClick, + enabled = enabled, + ) + fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text( text = text, onClicked = onTextClicked, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 5654a17174..6ed094c0cb 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -10,4 +10,5 @@ interface OnrampIntents { fun openProviders() fun onRefresh() fun onLinkClick(link: String) + fun onContinueClick() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 97f9d147c7..50acb8a5a6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -281,6 +281,8 @@ internal class OnrampMainComponentModel @Inject constructor( override fun onLinkClick(link: String) = urlOpener.openUrl(link) + override fun onContinueClick() = Unit + override fun onDestroy() { modelScope.launch { clearOnrampCacheUseCase.invoke() } quotesTaskScheduler.cancelTask() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt new file mode 100644 index 0000000000..e73a8570bd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.newmain + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +class DefaultOnrampNewMainFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) : OnrampNewMainFeatureToggle { + override val isOnrampNewMainEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED") +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt new file mode 100644 index 0000000000..d30d134f81 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.newmain + +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.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.OnrampSource + +internal interface OnrampNewMainComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val source: OnrampSource, + val openSettings: () -> Unit, + val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt new file mode 100644 index 0000000000..5eab7b94c5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onramp.newmain + +internal interface OnrampNewMainFeatureToggle { + val isOnrampNewMainEnabled: Boolean +} \ No newline at end of file From 2cd8786c752059177b1a178ac2d4220ca6bd39d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 13:30:29 +0500 Subject: [PATCH 26/48] Updated on 2026-08-14 --- .../grid/EnumeratedTwoColumnGrid.kt | 104 ++++++++++++++++++ .../entity/EnumeratedTwoColumnGridItem.kt | 6 + .../phrase/entity/ManualBackupPhraseUM.kt | 10 +- .../phrase/model/ManualBackupPhraseModel.kt | 3 +- .../phrase/ui/ManualBackupPhraseContent.kt | 76 +------------ .../GenerateSeedPhraseUiStateBuilder.kt | 9 +- .../ui/MultiWalletSeedPhraseWords.kt | 74 +------------ .../ui/state/MultiWalletSeedPhraseUM.kt | 10 +- 8 files changed, 133 insertions(+), 159 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/grid/EnumeratedTwoColumnGrid.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/grid/entity/EnumeratedTwoColumnGridItem.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/grid/EnumeratedTwoColumnGrid.kt b/core/ui/src/main/java/com/tangem/core/ui/components/grid/EnumeratedTwoColumnGrid.kt new file mode 100644 index 0000000000..f3c88b8cde --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/grid/EnumeratedTwoColumnGrid.kt @@ -0,0 +1,104 @@ +package com.tangem.core.ui.components.grid + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * EnumeratedTwoColumnGrid component + * + * @param items component items + * @param modifier composable modifier + * + */ +@Composable +fun EnumeratedTwoColumnGrid(items: ImmutableList, modifier: Modifier = Modifier) { + VerticalGrid( + modifier = modifier, + items = items, + ) { item -> + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.size8), + verticalAlignment = Alignment.CenterVertically, + ) { + if (LocalLayoutDirection.current == LayoutDirection.Ltr) { + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + } else { + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +@Composable +private inline fun VerticalGrid( + items: ImmutableList, + modifier: Modifier = Modifier, + crossinline content: @Composable (T) -> Unit, +) { + val columnLength = items.size / 2 + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + repeat(2) { index -> + Column { + for (i in 0 until columnLength) { + val item = items[index * columnLength + i] + content(item) + } + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640, showBackground = true) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + EnumeratedTwoColumnGrid( + items = List(24) { + EnumeratedTwoColumnGridItem( + index = it + 1, + mnemonic = "word${it + 1}", + ) + }.toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/grid/entity/EnumeratedTwoColumnGridItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/grid/entity/EnumeratedTwoColumnGridItem.kt new file mode 100644 index 0000000000..770855af97 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/grid/entity/EnumeratedTwoColumnGridItem.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.components.grid.entity + +data class EnumeratedTwoColumnGridItem( + val index: Int, + val mnemonic: String, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt index 3a799f4f31..a2b29d0220 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt @@ -1,14 +1,10 @@ package com.tangem.features.hotwallet.manualbackup.phrase.entity +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal data class ManualBackupPhraseUM( val onContinueClick: () -> Unit, - val words: ImmutableList = persistentListOf(), -) { - data class MnemonicGridItem( - val index: Int, - val mnemonic: String, - ) -} \ No newline at end of file + val words: ImmutableList = persistentListOf(), +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt index e9625260ad..0c99cd5606 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse 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.ui.components.grid.entity.EnumeratedTwoColumnGridItem import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent @@ -51,7 +52,7 @@ internal class ManualBackupPhraseModel @Inject constructor( uiState.update { it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> - ManualBackupPhraseUM.MnemonicGridItem(index + 1, s) + EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt index 3bba1be463..86458b8395 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt @@ -7,20 +7,18 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.impl.R import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @Composable @@ -41,8 +39,8 @@ internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Mo modifier = Modifier.padding(top = 20.dp), ) - SeedPhraseGridBlock( - mnemonicGridItems = state.words, + EnumeratedTwoColumnGrid( + items = state.words, modifier = Modifier .fillMaxWidth() .padding(top = 20.dp, bottom = 32.dp), @@ -97,70 +95,6 @@ private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifie } } -@Composable -private fun SeedPhraseGridBlock( - mnemonicGridItems: ImmutableList, - modifier: Modifier = Modifier, -) { - VerticalGrid( - modifier = modifier, - items = mnemonicGridItems, - ) { item -> - Row( - modifier = Modifier.padding(all = TangemTheme.dimens.size8), - verticalAlignment = Alignment.CenterVertically, - ) { - if (LocalLayoutDirection.current == LayoutDirection.Ltr) { - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) - } else { - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - } - } - } -} - -@Composable -private inline fun VerticalGrid( - items: ImmutableList, - modifier: Modifier = Modifier, - crossinline content: @Composable (T) -> Unit, -) { - val columnLength = items.size / 2 - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - repeat(2) { index -> - Column { - for (i in 0 until columnLength) { - val item = items[index * columnLength + i] - content(item) - } - } - } - } -} - @Preview(showBackground = true, widthDp = 360, heightDp = 640) @Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -170,7 +104,7 @@ private fun Preview() { state = ManualBackupPhraseUM( onContinueClick = {}, words = List(12) { - ManualBackupPhraseUM.MnemonicGridItem( + EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt index e7c4efd3b2..d5c6e72ba3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem import com.tangem.crypto.bip39.Mnemonic import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM @@ -20,10 +21,10 @@ internal class GenerateSeedPhraseUiStateBuilder( option: GeneratedWordsType, ): MultiWalletSeedPhraseUM.GenerateSeedPhrase { val words12 = generatedWords12.mnemonicComponents.mapIndexed { index, s -> - MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s) + EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList() val words24 = generatedWords24.mnemonicComponents.mapIndexed { index, s -> - MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s) + EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList() return MultiWalletSeedPhraseUM.GenerateSeedPhrase( @@ -45,8 +46,8 @@ internal class GenerateSeedPhraseUiStateBuilder( private fun switchType( newType: GeneratedWordsType, - generatedWords12: ImmutableList, - generatedWords24: ImmutableList, + generatedWords12: ImmutableList, + generatedWords24: ImmutableList, ) { updateUiState { uiSt -> changeGeneratedWordsType(newType) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt index 1099d8bcc1..039ddff2e5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt @@ -5,15 +5,14 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem import com.tangem.core.ui.extensions.pluralStringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -21,8 +20,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM -import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -45,8 +42,8 @@ internal fun MultiWalletSeedPhraseWords( TitleBlock(state) - SeedPhraseGridBlock( - mnemonicGridItems = state.words, + EnumeratedTwoColumnGrid( + items = state.words, modifier = Modifier .fillMaxWidth() .padding(top = 20.dp, bottom = 32.dp), @@ -127,67 +124,6 @@ private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifi } } -@Composable -private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList, modifier: Modifier = Modifier) { - VerticalGrid( - modifier = modifier, - items = mnemonicGridItems, - ) { item -> - Row( - modifier = Modifier.padding(all = TangemTheme.dimens.size8), - verticalAlignment = Alignment.CenterVertically, - ) { - if (LocalLayoutDirection.current == LayoutDirection.Ltr) { - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) - } else { - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - } - } - } -} - -@Composable -private inline fun VerticalGrid( - items: ImmutableList, - modifier: Modifier = Modifier, - crossinline content: @Composable (T) -> Unit, -) { - val columnLength = items.size / 2 - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - repeat(2) { index -> - Column { - for (i in 0 until columnLength) { - val item = items[index * columnLength + i] - content(item) - } - } - } - } -} - @Preview(showBackground = true, heightDp = 640) @Composable private fun Preview() { @@ -195,7 +131,7 @@ private fun Preview() { MultiWalletSeedPhraseWords( state = MultiWalletSeedPhraseUM.GenerateSeedPhrase( words = List(24) { - MnemonicGridItem( + EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word1", ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt index c167a59481..af9e46b850 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.s import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem import com.tangem.core.ui.extensions.TextReference import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType @@ -22,15 +23,10 @@ internal sealed class MultiWalletSeedPhraseUM( data class GenerateSeedPhrase( val option: GeneratedWordsType = GeneratedWordsType.Words12, - val words: ImmutableList = persistentListOf(), + val words: ImmutableList = persistentListOf(), val onOptionChange: (GeneratedWordsType) -> Unit = {}, val onContinueClick: () -> Unit = {}, - ) : MultiWalletSeedPhraseUM(order = 1) { - data class MnemonicGridItem( - val index: Int, - val mnemonic: String, - ) - } + ) : MultiWalletSeedPhraseUM(order = 1) data class GeneratedWordsCheck( val wordFields: ImmutableList = persistentListOf(), From 450db48cedc8fbe93b2ecc3d01b9458df442dc9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 11:56:40 +0300 Subject: [PATCH 27/48] Updated on 2026-08-14 --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0dd13a52a2..09eae1b14d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -74,7 +74,7 @@ platform :android do gradle( task: "assemble", - build_type: "Internal", + build_type: "GoogleInternal", properties: { 'versionCode' => ENV['version_code'], 'versionName' => ENV['version_name'], From 7a5996534255b9c33a8daedd4a508ad69dcaa697 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 09:44:54 +0000 Subject: [PATCH 28/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 05f6e8b112..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.28-1211" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From d8a0703e4602bce73e5c0e824964d04b19350ca3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 12:10:34 +0000 Subject: [PATCH 29/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 05f6e8b112..33e667f9d0 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.28-1211" +tangemBlockchainSdk = "develop-1212" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +tangemCardSdk = "develop-560" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From e250ecd50824222bbaf6b9198f4e8b80f5c585c0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 16:07:49 +0300 Subject: [PATCH 30/48] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 22 ++++++++++++++++--- .../walletconnect/WalletConnectMiddleware.kt | 17 +++++++++++--- .../ui/resetcard/model/ResetCardModel.kt | 16 +++++++++----- .../tangem/tap/features/main/MainViewModel.kt | 22 +++++++++++-------- .../CryptoCurrencyConverter.kt | 20 ++++++++++++----- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 8 +++---- .../com/tangem/tap/proxy/di/ProxyModule.kt | 6 ++--- .../feedback/DefaultFeedbackRepository.kt | 4 +++- .../tangem/data/feedback/di/FeedbackModule.kt | 3 +++ .../CreateWalletSelectionModel.kt | 8 +++---- .../features/home/impl/model/HomeModel.kt | 16 ++++++++++++-- .../start/AddExistingWalletStartModel.kt | 8 +++---- .../entry/impl/model/OnboardingEntryModel.kt | 17 ++++++++++++++ 13 files changed, 123 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 9df63ef08a..e6e92ccda4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -47,6 +47,14 @@ class WalletConnectSdkHelper { store.inject(DaggerGraphState::generalUserWalletsListManager) } + private val userWalletsListRepository by lazy { + store.inject(DaggerGraphState::userWalletsListRepository) + } + + private val hotWalletFeatureToggles by lazy { + store.inject(DaggerGraphState::hotWalletFeatureToggles) + } + @Suppress("MagicNumber") suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData { val transaction = data.transaction @@ -128,13 +136,13 @@ class WalletConnectSdkHelper { ) } - fun isDemoCard(): Boolean { - val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false + suspend fun isDemoCard(): Boolean { + val userWallet = getSelectedWallet() ?: return false return userWallet is UserWallet.Cold && userWallet.scanResponse.isDemoCard() } private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? { - val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null + val userWallet = getSelectedWallet() ?: return null val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) return walletManagerFacade.getOrCreateWalletManager( userWalletId = userWallet.walletId, @@ -481,6 +489,14 @@ class WalletConnectSdkHelper { } } + private suspend fun getSelectedWallet(): UserWallet? { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } + private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }" /** diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 7439241a50..499e1861b0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.routing.AppRoute +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject @@ -183,9 +184,19 @@ class WalletConnectMiddleware { private suspend fun getWalletManagers(): List { val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() - + val userWallet = getSelectedWallet() ?: return emptyList() return walletManagerFacade.getStoredWalletManagers(userWallet.walletId) } + + private suspend fun getSelectedWallet(): UserWallet? { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) + + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index 2d0af3ca0b..6cab06fac6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.onUserWalletSelected @@ -50,6 +51,7 @@ internal class ResetCardModel @Inject constructor( private val userWalletsListManager: UserWalletsListManager, private val analyticsEventHandler: AnalyticsEventHandler, private val cardSettingsInteractor: CardSettingsInteractor, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -259,16 +261,20 @@ internal class ResetCardModel @Inject constructor( private fun finishFullReset() { cardSettingsInteractor.clear() - val newSelectedWallet = userWalletsListManager.selectedUserWalletSync + val newSelectedWallet = getSelectedWalletSyncUseCase.invoke().getOrNull() if (newSelectedWallet != null) { store.dispatchNavigationAction { popTo() } } else { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess - if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchNavigationAction { popTo() } - } else { + if (hotWalletFeatureToggles.isHotWalletEnabled) { store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } + } else { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess + if (isLocked && userWalletsListManager.hasUserWallets) { + store.dispatchNavigationAction { popTo() } + } else { + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 643710e4c2..777ff7cc9a 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -24,6 +24,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.common.LogConfig +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.models.ApplicationId @@ -37,9 +38,9 @@ import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.tap.common.extensions.setContext @@ -69,7 +70,6 @@ internal class MainViewModel @Inject constructor( deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, private val blockchainSDKFactory: BlockchainSDKFactory, - private val userWalletsListManager: UserWalletsListManager, private val dispatchers: CoroutineDispatcherProvider, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val fetchUserCountryUseCase: FetchUserCountryUseCase, @@ -90,6 +90,7 @@ internal class MainViewModel @Inject constructor( private val multiQuoteUpdater: MultiQuoteUpdater, private val appStateHolder: AppStateHolder, private val environmentConfigStorage: EnvironmentConfigStorage, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -185,13 +186,16 @@ internal class MainViewModel @Inject constructor( } private fun prepareSelectedWalletFeedback() { - userWalletsListManager.selectedUserWallet - .distinctUntilChanged() - .onEach { userWallet -> - Analytics.setContext(userWallet) + getSelectedWalletUseCase.invoke() + .mapLeft { emptyFlow() } + .onRight { + it.distinctUntilChanged() + .onEach { userWallet -> + Analytics.setContext(userWallet) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) } private suspend fun fetchStakingTokens() { @@ -214,7 +218,7 @@ internal class MainViewModel @Inject constructor( apiKey = environmentConfig.moonPayApiKey, secretKey = environmentConfig.moonPayApiSecretKey, logEnabled = LogConfig.network.moonPayService, - userWalletProvider = { userWalletsListManager.selectedUserWalletSync }, + userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() }, ) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index f71c4160f9..97cb5f65e6 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency import com.tangem.tap.proxy.redux.DaggerGraphState @@ -24,9 +25,7 @@ internal class CryptoCurrencyConverter( cryptoCurrencyFactory.createCoin( blockchain = value.blockchain, extraDerivationPath = value.derivationPath, - userWallet = requireNotNull( - store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync, - ), + userWallet = getSelectedWallet(), ), ) is Currency.Token -> requireNotNull( @@ -34,9 +33,7 @@ internal class CryptoCurrencyConverter( sdkToken = value.token, blockchain = value.blockchain, extraDerivationPath = value.derivationPath, - userWallet = requireNotNull( - store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync, - ), + userWallet = getSelectedWallet(), ), ) } @@ -63,4 +60,15 @@ internal class CryptoCurrencyConverter( ) } } + + fun getSelectedWallet(): UserWallet { + val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + requireNotNull(userWalletsListRepository.selectedUserWallet.value) + } else { + requireNotNull(userWalletListManager.selectedUserWalletSync) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 767198d50e..d11526b9f4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -15,13 +15,13 @@ import java.math.BigDecimal class UserWalletManagerImpl( private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletManager { override fun getWalletId(): String { val selectedUserWallet = requireNotNull( - userWalletsListManager.selectedUserWalletSync, + getSelectedWalletUseCase.sync().getOrNull(), ) { "selectedUserWallet shouldn't be null" } return selectedUserWallet.walletId.stringValue } @@ -62,7 +62,7 @@ class UserWalletManagerImpl( @Throws(IllegalArgumentException::class) private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val selectedUserWallet = requireNotNull( - userWalletsListManager.selectedUserWalletSync, + getSelectedWalletUseCase.sync().getOrNull(), ) { "userWallet or userWalletsListManager is null" } val walletManager = withContext(dispatchers.io) { walletManagersFacade.getOrCreateWalletManager( diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 4d59c1f56e..677d7e5ebd 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.proxy.di import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.lib.crypto.UserWalletManager import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.UserWalletManagerImpl @@ -26,12 +26,12 @@ internal object ProxyModule { @Singleton fun provideUserWalletManager( walletManagersFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, + getSelectedWalletUseCase: GetSelectedWalletUseCase, dispatchers: CoroutineDispatcherProvider, ): UserWalletManager { return UserWalletManagerImpl( walletManagersFacade = walletManagersFacade, - userWalletsListManager = userWalletsListManager, + getSelectedWalletUseCase = getSelectedWalletUseCase, dispatchers = dispatchers, ) } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 5de6bede2a..c64229e83a 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -12,6 +12,7 @@ import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.utils.version.AppVersionProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -35,6 +36,7 @@ internal class DefaultFeedbackRepository( private val walletManagersStore: WalletManagersStore, private val emailSender: EmailSender, private val appVersionProvider: AppVersionProvider, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, ) : FeedbackRepository { private val blockchainsErrors = MutableStateFlow>(emptyMap()) @@ -77,7 +79,7 @@ internal class DefaultFeedbackRepository( } override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { - val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected") + val userWallet = getSelectedWalletUseCase.sync().getOrNull() ?: error("UserWallet is not selected") blockchainsErrors.update { it.toMutableMap().apply { diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index cd701c4695..fd2bc36968 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -28,6 +29,7 @@ internal object FeedbackModule { walletManagersStore: WalletManagersStore, emailSender: EmailSender, appVersionProvider: AppVersionProvider, + getSelectedWalletUseCase: GetSelectedWalletUseCase, ): FeedbackRepository { return DefaultFeedbackRepository( appLogsStore = appLogsStore, @@ -35,6 +37,7 @@ internal object FeedbackModule { walletManagersStore = walletManagersStore, emailSender = emailSender, appVersionProvider = appVersionProvider, + getSelectedWalletUseCase = getSelectedWalletUseCase, ) } diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 23d94a6c78..10e571261d 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -23,11 +23,11 @@ import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM @@ -56,7 +56,7 @@ internal class CreateWalletSelectionModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -146,7 +146,7 @@ internal class CreateWalletSelectionModel @Inject constructor( ) } - private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) if (currency != null) { analyticsEventHandler.send( @@ -154,7 +154,7 @@ internal class CreateWalletSelectionModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = userWalletsListManager.walletsCount.toString(), + walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 10ae9abbf8..5c73a404c1 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder @@ -41,6 +42,7 @@ import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -76,6 +78,8 @@ internal class HomeModel @Inject constructor( private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, private val userWalletsListManager: UserWalletsListManager, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, private val reduxStateHolder: ReduxStateHolder, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -216,7 +220,7 @@ internal class HomeModel @Inject constructor( ) } - private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) if (currency != null) { analyticsEventHandler.send( @@ -224,13 +228,21 @@ internal class HomeModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = userWalletsListManager.walletsCount.toString(), + walletsCount = getWalletsCount().toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) } } + private suspend fun getWalletsCount(): Int { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWalletsSync().size + } else { + userWalletsListManager.walletsCount + } + } + private fun setLoading(isLoading: Boolean) { _uiState.update { it.copy(scanInProgress = isLoading) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index dab15c9946..9f56cce80b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -23,11 +23,11 @@ import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM @@ -56,7 +56,7 @@ internal class AddExistingWalletStartModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val appRouter: AppRouter, private val urlOpener: UrlOpener, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -145,7 +145,7 @@ internal class AddExistingWalletStartModel @Inject constructor( ) } - private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) if (currency != null) { analyticsEventHandler.send( @@ -153,7 +153,7 @@ internal class AddExistingWalletStartModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = userWalletsListManager.walletsCount.toString(), + walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 24dc8aaa95..e1aca5db5e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -13,12 +13,14 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent @@ -46,6 +48,8 @@ internal class OnboardingEntryModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, private val userWalletsListManager: UserWalletsListManager, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { private val params = paramsContainer.require() @@ -197,6 +201,19 @@ internal class OnboardingEntryModel @Inject constructor( } private fun exitComponentScreen() { + // new flow + if (hotWalletFeatureToggles.isHotWalletEnabled) { + modelScope.launch { + if (userWalletsListRepository.userWalletsSync().isEmpty()) { + router.replaceAll(AppRoute.Home()) + } else { + router.replaceAll(AppRoute.Wallet) + } + } + return + } + + // legacy flow if (userWalletsListManager.hasUserWallets) { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } From 41862d07ee097275ef3fcce636a4018aaecb5cea Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 13:11:29 +0000 Subject: [PATCH 31/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 05f6e8b112..33e667f9d0 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.28-1211" +tangemBlockchainSdk = "develop-1212" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +tangemCardSdk = "develop-560" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From dfc3dbc324447651669a049eed2b2e7de14dc267 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 20:34:35 +0700 Subject: [PATCH 32/48] Updated on 2026-08-14 --- .../component/AddCustomTokenComponent.kt | 3 +- .../component/ManageTokensComponent.kt | 11 +- .../component/ManageTokensSource.kt | 14 ++ .../model/ChooseManagedTokensModel.kt | 15 +- .../CustomTokenDerivationInputComponent.kt | 3 +- .../component/CustomTokenFormComponent.kt | 3 +- .../component/CustomTokenSelectorComponent.kt | 5 +- .../impl/DefaultAddCustomTokenComponent.kt | 10 +- .../DefaultCustomTokenSelectorComponent.kt | 2 +- .../impl/DefaultManageTokensComponent.kt | 25 +-- .../preview/PreviewAddCustomTokenComponent.kt | 9 +- .../PreviewCustomTokenSelectorComponent.kt | 3 +- .../preview/PreviewManageTokensComponent.kt | 10 +- .../customtoken/AddCustomTokenConfig.kt | 4 +- .../CustomTokenSelectorDialogConfig.kt | 4 +- .../ManageTokensBottomSheetConfig.kt | 8 +- .../model/CustomTokenFormModel.kt | 20 +- .../model/CustomTokenSelectorModel.kt | 13 +- .../managetokens/model/ManageTokensModel.kt | 67 ++++--- .../model/OnboardingManageTokensModel.kt | 44 ++--- .../ui/AddCustomTokenBottomSheet.kt | 8 +- .../ui/CustomTokenSelectorContent.kt | 6 +- .../utils/CustomCurrencyValidator.kt | 57 ++---- .../list/CustomTokenFormUseCasesFacade.kt | 117 ++++++++++++ .../utils/list/ManageTokensListManager.kt | 178 ++++++------------ .../utils/list/ManageTokensListState.kt | 4 +- .../utils/list/ManageTokensUiActions.kt | 6 +- .../utils/list/ManageTokensUiManager.kt | 88 +-------- .../utils/list/ManageTokensUseCasesFacade.kt | 108 +++++++++++ .../utils/list/ManageTokensWarningDelegate.kt | 98 ++++++++++ 30 files changed, 574 insertions(+), 369 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index aad2b8562d..c45a8667f6 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -2,12 +2,11 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.domain.models.wallet.UserWalletId interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val source: ManageTokensSource, val onDismiss: () -> Unit, val onCurrencyAdded: () -> Unit, diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index a02c7125ff..e680fabf26 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -7,9 +7,16 @@ import com.tangem.domain.models.wallet.UserWalletId interface ManageTokensComponent : ComposableContentComponent { data class Params( - val userWalletId: UserWalletId?, + val mode: ManageTokensMode, val source: ManageTokensSource, - ) + ) { + constructor(userWalletId: UserWalletId?, source: ManageTokensSource) : this( + source = source, + mode = userWalletId + ?.let { ManageTokensMode.Wallet(userWalletId) } + ?: ManageTokensMode.None, + ) + } 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 c1d317c3d8..3c34dd0f34 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 @@ -1,8 +1,22 @@ package com.tangem.features.managetokens.component +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId + enum class ManageTokensSource(val analyticsName: String) { STORIES(analyticsName = "Stories"), ONBOARDING(analyticsName = "Onboarding"), SETTINGS(analyticsName = "Settings"), SEND_VIA_SWAP(analyticsName = "SendViaSwap"), +} + +sealed interface ManageTokensMode { + data class Wallet(val userWalletId: UserWalletId) : ManageTokensMode + data class Account(val accountId: AccountId) : ManageTokensMode + data object None : ManageTokensMode +} + +sealed interface AddCustomTokenMode { + data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode + data class Account(val accountId: AccountId) : AddCustomTokenMode } \ 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 index 3c239162b1..a9c9f03c8f 100644 --- 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 @@ -22,6 +22,7 @@ import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBot 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.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -29,6 +30,7 @@ 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.ManageTokensUseCasesFacade import com.tangem.features.managetokens.utils.list.getLoadingItems import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus @@ -51,12 +53,19 @@ internal class ChooseManagedTokensModel @Inject constructor( private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, + manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { private val params: ChooseManagedTokensComponent.Params = paramsContainer.require() + private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory + .create(mode = ManageTokensMode.None) private val manageTokensListManager = manageTokensListManagerFactory.create( + scope = modelScope, + source = ManageTokensSource.SEND_VIA_SWAP, + mode = ManageTokensMode.None, + useCasesFacade = useCasesFacade, onCurrencySelect = { token -> bottomSheetNavigation.activate( ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig( @@ -86,7 +95,7 @@ internal class ChooseManagedTokensModel @Inject constructor( observeSearchQueryChanges() modelScope.launch { - manageTokensListManager.launchPagination(source = ManageTokensSource.SEND_VIA_SWAP, userWalletId = null) + manageTokensListManager.launchPagination() } } @@ -159,7 +168,7 @@ internal class ChooseManagedTokensModel @Inject constructor( } } .sample(periodMillis = 1_000) - .onEach { query -> manageTokensListManager.search(userWalletId = null, query = query) } + .onEach { query -> manageTokensListManager.search(query = query) } .launchIn(modelScope) } @@ -295,7 +304,7 @@ internal class ChooseManagedTokensModel @Inject constructor( if (state.readContent.isInitialBatchLoading || state.readContent.isNextBatchLoading) return false modelScope.launch { - manageTokensListManager.loadMore(userWalletId = null, query = state.readContent.search.query) + manageTokensListManager.loadMore(query = state.readContent.search.query) } return true diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt index 7ec0cff9c9..56f2b08ca9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt @@ -2,13 +2,12 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableDialogComponent -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent { data class Params( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val onConfirm: (SelectedDerivationPath) -> Unit, val onDismiss: () -> Unit, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 04a9e59d48..2baf3f982f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -2,7 +2,6 @@ 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.wallet.UserWalletId import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork @@ -10,7 +9,7 @@ import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork internal interface CustomTokenFormComponent : ComposableContentComponent { data class Params( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val network: SelectedNetwork, val derivationPath: SelectedDerivationPath?, val formValues: CustomTokenFormValues, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt index 6e36ef8eb7..a4ec53c9d9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt @@ -2,7 +2,6 @@ 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.wallet.UserWalletId import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork @@ -11,13 +10,13 @@ internal interface CustomTokenSelectorComponent : ComposableContentComponent { sealed class Params { data class NetworkSelector( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val selectedNetwork: SelectedNetwork?, val onNetworkSelected: (SelectedNetwork) -> Unit, ) : Params() data class DerivationPathSelector( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val selectedNetwork: SelectedNetwork, val selectedDerivationPath: SelectedDerivationPath?, val onDerivationPathSelected: (SelectedDerivationPath) -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index f4ecc9ceed..1951b3cd44 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -38,7 +38,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( ) : AddCustomTokenComponent, AppComponentContext by context { private val initialConfiguration = AddCustomTokenConfig( - userWalletId = params.userWalletId, + mode = params.mode, step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, ) @@ -105,7 +105,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( selectorComponentFactory.create( context = childByContext(componentContext), params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = null, onNetworkSelected = ::changeSelectedNetwork, ), @@ -115,7 +115,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( selectorComponentFactory.create( context = childByContext(componentContext), params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = config.selectedNetwork, onNetworkSelected = ::changeSelectedNetwork, ), @@ -125,7 +125,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( selectorComponentFactory.create( context = childByContext(componentContext), params = CustomTokenSelectorComponent.Params.DerivationPathSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = requireNotNull(config.selectedNetwork) { "Network is not selected" }, @@ -138,7 +138,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( formComponentFactory.create( context = childByContext(componentContext), params = CustomTokenFormComponent.Params( - userWalletId = config.userWalletId, + mode = config.mode, network = requireNotNull(config.selectedNetwork) { "Network is not selected" }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt index edd8f52aa9..44c781e3cc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt @@ -41,7 +41,7 @@ internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor( is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create( context = childByContext(context), params = CustomTokenDerivationInputComponent.Params( - userWalletId = config.userWalletId, + mode = config.mode, onConfirm = model::selectCustomDerivationPath, onDismiss = model.dialogNavigation::dismiss, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 76ae54a12d..9dc5539ae4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.model.ManageTokensModel @@ -52,18 +53,20 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( private fun bottomSheetChild( config: ManageTokensBottomSheetConfig, componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - is ManageTokensBottomSheetConfig.AddCustomToken -> { - addCustomTokenComponentFactory.create( - context = childByContext(componentContext), - params = AddCustomTokenComponent.Params( - userWalletId = config.userWalletId, - source = params.source, - onDismiss = model.bottomSheetNavigation::dismiss, - onCurrencyAdded = model::reloadList, - ), - ) + ): ComposableBottomSheetComponent { + val mode = when (config) { + is ManageTokensBottomSheetConfig.AddWalletCustomToken -> AddCustomTokenMode.Wallet(config.userWalletId) + is ManageTokensBottomSheetConfig.AddAccountCustomToken -> AddCustomTokenMode.Account(config.accountId) } + return addCustomTokenComponentFactory.create( + context = childByContext(componentContext), + params = AddCustomTokenComponent.Params( + mode = mode, + source = params.source, + onDismiss = model.bottomSheetNavigation::dismiss, + onCurrencyAdded = model::reloadList, + ), + ) } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index eb693e20b1..239866349d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet @@ -14,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class PreviewAddCustomTokenComponent( initialState: AddCustomTokenConfig = AddCustomTokenConfig( - userWalletId = UserWalletId(stringValue = "321"), + mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, ), ) : AddCustomTokenComponent { @@ -41,7 +42,7 @@ internal class PreviewAddCustomTokenComponent( AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> { PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = null, onNetworkSelected = {}, ), @@ -50,7 +51,7 @@ internal class PreviewAddCustomTokenComponent( AddCustomTokenConfig.Step.NETWORK_SELECTOR -> { PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = config.selectedNetwork, onNetworkSelected = {}, ), @@ -59,7 +60,7 @@ internal class PreviewAddCustomTokenComponent( AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> { PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( - userWalletId = config.userWalletId, + mode = config.mode, selectedNetwork = config.selectedNetwork!!, selectedDerivationPath = config.selectedDerivationPath!!, onDerivationPathSelected = {}, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index eca3ff5cc5..9d4485dee8 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM @@ -18,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList internal class PreviewCustomTokenSelectorComponent( private val params: Params = Params.NetworkSelector( - userWalletId = UserWalletId(stringValue = "321"), + mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), selectedNetwork = null, onNetworkSelected = {}, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index d7caaf672c..c14f0f1836 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM @@ -38,8 +39,10 @@ internal class PreviewManageTokensComponent( value = ManageTokensUM.ManageContent( popBack = {}, items = items, - topBar = if (params.userWalletId != null) { - ManageTokensTopBarUM.ManageContent( + topBar = when (params.mode) { + is ManageTokensMode.Account, + is ManageTokensMode.Wallet, + -> ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = {}, endButton = TopAppBarButtonUM.Icon( @@ -47,8 +50,7 @@ internal class PreviewManageTokensComponent( onClicked = {}, ), ) - } else { - ManageTokensTopBarUM.ReadContent( + ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = {}, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 767e87805c..e33d46321d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -1,13 +1,13 @@ package com.tangem.features.managetokens.entity.customtoken import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenMode import kotlinx.serialization.Serializable @Serializable internal data class AddCustomTokenConfig( val step: Step, - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, val selectedNetwork: SelectedNetwork? = null, val selectedDerivationPath: SelectedDerivationPath? = null, val formValues: CustomTokenFormValues = CustomTokenFormValues(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt index 05a447f9d1..fc34e8cdda 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt @@ -1,6 +1,6 @@ package com.tangem.features.managetokens.entity.customtoken -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenMode import kotlinx.serialization.Serializable @Serializable @@ -8,6 +8,6 @@ internal sealed class CustomTokenSelectorDialogConfig { @Serializable data class CustomDerivationInput( - val userWalletId: UserWalletId, + val mode: AddCustomTokenMode, ) : CustomTokenSelectorDialogConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt index 9e2153b6e5..91a4b7fba3 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.entity.managetokens +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -7,7 +8,12 @@ import kotlinx.serialization.Serializable internal sealed class ManageTokensBottomSheetConfig { @Serializable - data class AddCustomToken( + data class AddWalletCustomToken( val userWalletId: UserWalletId, ) : ManageTokensBottomSheetConfig() + + @Serializable + data class AddAccountCustomToken( + val accountId: AccountId, + ) : ManageTokensBottomSheetConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 08d5f86574..6e5d19d000 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -12,9 +12,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM @@ -25,6 +22,7 @@ import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.CustomCurrencyFormBuilder import com.tangem.features.managetokens.utils.CustomCurrencyValidator +import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade import com.tangem.features.managetokens.utils.mapper.mapToDomainModel import com.tangem.features.managetokens.utils.ui.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,18 +38,17 @@ import javax.inject.Inject @ModelScoped internal class CustomTokenFormModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val customCurrencyValidator: CustomCurrencyValidator, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val messageSender: UiMessageSender, private val customTokenFormManager: CustomCurrencyFormBuilder, private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, + customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory, ) : Model() { private val params: CustomTokenFormComponent.Params = paramsContainer.require() private var createdCurrency: CryptoCurrency? = null + private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode) + private val customCurrencyValidator = CustomCurrencyValidator(useCasesFacade) val state: MutableStateFlow = MutableStateFlow( value = getInitialState(), @@ -117,7 +114,6 @@ internal class CustomTokenFormModel @Inject constructor( .drop(count = 1) // Skip initial state .onEach { formValues -> customCurrencyValidator.validateForm( - userWalletId = params.userWalletId, networkId = params.network.id, derivationPath = getDerivationPath(), formValues = formValues, @@ -156,7 +152,6 @@ internal class CustomTokenFormModel @Inject constructor( private fun validatePrefilledForm() = modelScope.launch { customCurrencyValidator.validateForm( - userWalletId = params.userWalletId, networkId = params.network.id, derivationPath = getDerivationPath(), formValues = state.value.tokenForm.mapToDomainModel(), @@ -169,8 +164,7 @@ internal class CustomTokenFormModel @Inject constructor( isAlreadyAdded: Boolean, isCustom: Boolean, ) = modelScope.launch { - val needToAddDerivation = hasMissedDerivationsUseCase( - userWalletId = params.userWalletId, + val needToAddDerivation = useCasesFacade.hasMissedDerivationsUseCase( networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), ) @@ -343,13 +337,13 @@ internal class CustomTokenFormModel @Inject constructor( ) analyticsEventHandler.send(event) - derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse { + useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { Timber.e(it, "Failed to derive public keys") showErrorDialog() return@resource } - addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse { + useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { Timber.e(it, "Failed to add currency") showErrorDialog() return@resource diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index a919429bd1..3bb119e580 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.GetSupportedNetworksUseCase import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector @@ -83,7 +83,7 @@ internal class CustomTokenSelectorModel @Inject constructor( } private suspend fun loadNetworks(selector: NetworkSelector): List { - return getSupportedNetworks(selector.userWalletId).map { network -> + return getSupportedNetworks(selector.mode).map { network -> network.toCurrencyNetworkModel( isSelected = network.id == selector.selectedNetwork?.id, onSelectedStateChange = { @@ -122,7 +122,7 @@ internal class CustomTokenSelectorModel @Inject constructor( derivationPaths.add(defaultPath) } - getSupportedNetworks(selector.userWalletId) + getSupportedNetworks(selector.mode) .mapNotNullTo(derivationPaths) { network -> if (network.id == selector.selectedNetwork.id) { return@mapNotNullTo null // Skip default path @@ -146,8 +146,9 @@ internal class CustomTokenSelectorModel @Inject constructor( return derivationPaths } - private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { - return getSupportedNetworksUseCase(userWalletId).getOrElse { e -> + private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) messageSender.send(message) @@ -158,7 +159,7 @@ internal class CustomTokenSelectorModel @Inject constructor( private fun showCustomDerivationInput() { val config = when (params) { is NetworkSelector -> return - is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId) + is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.mode) } dialogNavigation.activate(config) 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 91e1814599..afc1d11bc5 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 @@ -17,12 +17,10 @@ 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.domain.managetokens.SaveManagedTokensUseCase -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM @@ -30,6 +28,7 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.list.ChangedCurrencies import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.features.managetokens.utils.list.ManageTokensUseCasesFacade import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -47,18 +46,24 @@ internal class ManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val messageSender: UiMessageSender, - private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, - private val saveManagedTokensUseCase: SaveManagedTokensUseCase, private val analyticsEventHandler: AnalyticsEventHandler, manageTokensListManagerFactory: ManageTokensListManager.Factory, + manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params: ManageTokensComponent.Params = paramsContainer.require() + private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory + .create(mode = params.mode) - private val manageTokensListManager = manageTokensListManagerFactory.create() + private val manageTokensListManager = manageTokensListManagerFactory.create( + scope = modelScope, + source = params.source, + mode = params.mode, + useCasesFacade = useCasesFacade, + ) - val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) + val state: MutableStateFlow = MutableStateFlow(getInitialState()) val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { @@ -79,23 +84,24 @@ internal class ManageTokensModel @Inject constructor( observeSearchQueryChanges() modelScope.launch { - manageTokensListManager.launchPagination(source = params.source, userWalletId = params.userWalletId) + manageTokensListManager.launchPagination() } } fun reloadList() { modelScope.launch { - manageTokensListManager.reload(params.userWalletId) + manageTokensListManager.reload() } } - private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { + private fun getInitialState(): ManageTokensUM { analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source)) - return if (userWalletId == null) { - createReadContentModel() - } else { - createManageContentModel() + return when (params.mode) { + is ManageTokensMode.Wallet, + is ManageTokensMode.Account, + -> createManageContentModel() + ManageTokensMode.None -> createReadContentModel() } } @@ -164,7 +170,7 @@ internal class ManageTokensModel @Inject constructor( } } .sample(periodMillis = 1_000) - .onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) } + .onEach { query -> manageTokensListManager.search(query = query) } .launchIn(modelScope) } @@ -261,19 +267,16 @@ internal class ManageTokensModel @Inject constructor( private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { modelScope.launch { - val hasMissedDerivations = params.userWalletId?.let { walletId -> - val networks = currenciesToAdd.values - .flatten() - .toSet() - .associate { it.backendId to null } - - hasMissedDerivationsUseCase(walletId, networks) - } + val networks = currenciesToAdd.values + .flatten() + .toSet() + .associate { it.backendId to null } + val hasMissedDerivations = useCasesFacade.hasMissedDerivationsUseCase(networks) state.update { state -> state.copySealed( hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), - needToAddDerivations = hasMissedDerivations ?: false, + needToAddDerivations = hasMissedDerivations, ) } } @@ -284,7 +287,7 @@ internal class ManageTokensModel @Inject constructor( if (state.isInitialBatchLoading || state.isNextBatchLoading) return false modelScope.launch { - manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query) + manageTokensListManager.loadMore(query = state.search.query) } return true @@ -292,9 +295,14 @@ internal class ManageTokensModel @Inject constructor( private fun navigateToAddCustomToken() { analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source)) - - params.userWalletId?.let { - bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it)) + when (val portfolio = params.mode) { + is ManageTokensMode.Wallet -> + bottomSheetNavigation + .activate(ManageTokensBottomSheetConfig.AddWalletCustomToken(portfolio.userWalletId)) + is ManageTokensMode.Account -> + bottomSheetNavigation + .activate(ManageTokensBottomSheetConfig.AddAccountCustomToken(portfolio.accountId)) + ManageTokensMode.None -> Unit } } @@ -308,8 +316,7 @@ internal class ManageTokensModel @Inject constructor( ) analyticsEventHandler.send(event) - saveManagedTokensUseCase( - userWalletId = requireNotNull(params.userWalletId), + useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { 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 30f64dad46..101e04e78c 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 @@ -13,11 +13,10 @@ 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.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.redux.OnboardingManageTokensAction import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -25,6 +24,7 @@ import com.tangem.features.managetokens.entity.managetokens.OnboardingManageToke import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.list.ChangedCurrencies import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.features.managetokens.utils.list.ManageTokensUseCasesFacade import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -42,15 +42,22 @@ internal class OnboardingManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val messageSender: UiMessageSender, private val reduxStateHolder: ReduxStateHolder, - private val saveManagedTokensUseCase: SaveManagedTokensUseCase, - private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, manageTokensListManagerFactory: ManageTokensListManager.Factory, + manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params: OnboardingManageTokensComponent.Params = paramsContainer.require() - private val manageTokensListManager = manageTokensListManagerFactory.create() + private val portfolio = ManageTokensMode.Wallet(params.userWalletId) + private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory + .create(mode = portfolio) + private val manageTokensListManager = manageTokensListManagerFactory.create( + scope = modelScope, + source = ManageTokensSource.ONBOARDING, + useCasesFacade = useCasesFacade, + mode = portfolio, + ) val state: MutableStateFlow = MutableStateFlow(getInitialState()) val returnToParentComponentFlow = MutableSharedFlow() @@ -73,10 +80,7 @@ internal class OnboardingManageTokensModel @Inject constructor( observeSearchQueryChanges() modelScope.launch { - manageTokensListManager.launchPagination( - source = ManageTokensSource.ONBOARDING, - userWalletId = params.userWalletId, - ) + manageTokensListManager.launchPagination() } } @@ -119,7 +123,7 @@ internal class OnboardingManageTokensModel @Inject constructor( } } .sample(periodMillis = 1_000) - .onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) } + .onEach { query -> manageTokensListManager.search(query = query) } .launchIn(modelScope) } @@ -208,13 +212,11 @@ internal class OnboardingManageTokensModel @Inject constructor( ) } } else { - val hasMissedDerivations = hasMissedDerivationsUseCase.invoke( - userWalletId = params.userWalletId, - networksWithDerivationPath = currenciesToAdd.values - .flatten() - .toSet() - .associate { it.backendId to null }, - ) + val network = currenciesToAdd.values + .flatten() + .toSet() + .associate { it.backendId to null } + val hasMissedDerivations = useCasesFacade.hasMissedDerivationsUseCase(network = network) state.update { state -> state.copy( actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue( @@ -231,7 +233,7 @@ internal class OnboardingManageTokensModel @Inject constructor( if (state.isInitialBatchLoading || state.isNextBatchLoading) return false modelScope.launch { - manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query) + manageTokensListManager.loadMore(query = state.search.query) } return true @@ -255,8 +257,7 @@ internal class OnboardingManageTokensModel @Inject constructor( ) analyticsEventHandler.send(event) - saveManagedTokensUseCase( - userWalletId = requireNotNull(params.userWalletId), + useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { @@ -281,8 +282,7 @@ internal class OnboardingManageTokensModel @Inject constructor( ) { analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater) - saveManagedTokensUseCase( - userWalletId = requireNotNull(params.userWalletId), + useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index 8cd0a27d68..4699454540 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenUM @@ -68,12 +69,13 @@ private fun Preview_AddCustomTokenBottomSheet( } private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider { + private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( PreviewAddCustomTokenComponent(), PreviewAddCustomTokenComponent( initialState = AddCustomTokenConfig( - userWalletId = UserWalletId(stringValue = "321"), + mode = mode, step = AddCustomTokenConfig.Step.FORM, selectedNetwork = SelectedNetwork( id = Network.ID(value = "1", derivationPath = Network.DerivationPath.None), @@ -85,7 +87,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< ), PreviewAddCustomTokenComponent( initialState = AddCustomTokenConfig( - userWalletId = UserWalletId(stringValue = "321"), + mode = mode, step = AddCustomTokenConfig.Step.NETWORK_SELECTOR, selectedNetwork = SelectedNetwork( id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None), @@ -97,7 +99,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< ), PreviewAddCustomTokenComponent( initialState = AddCustomTokenConfig( - userWalletId = UserWalletId(stringValue = "321"), + mode = mode, step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR, selectedDerivationPath = SelectedDerivationPath( id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 6320fd6743..b7eae1ce2c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM @@ -268,12 +269,13 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewParameterProvider { private val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0") + private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( - userWalletId = UserWalletId(stringValue = "321"), + mode = mode, selectedNetwork = SelectedNetwork( id = Network.ID(value = "0", derivationPath = derivationPath), name = "Ethereum", @@ -291,7 +293,7 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : ), PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = UserWalletId(stringValue = "321"), + mode = mode, selectedNetwork = SelectedNetwork( id = Network.ID(value = "0", derivationPath = derivationPath), name = "Ethereum", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index bed3b20286..feabc9cb4d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -1,31 +1,21 @@ package com.tangem.features.managetokens.utils import arrow.core.getOrElse -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase -import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase -import com.tangem.domain.managetokens.FindTokenUseCase -import com.tangem.domain.managetokens.ValidateTokenFormUseCase import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.managetokens.model.exceptoin.FindTokenException import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveInAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -@ModelScoped -internal class CustomCurrencyValidator @Inject constructor( - private val validateTokenFormUseCase: ValidateTokenFormUseCase, - private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, - private val findTokenUseCase: FindTokenUseCase, - private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, +internal class CustomCurrencyValidator( + private val useCasesFacade: CustomTokenFormUseCasesFacade, ) { private val validateFormJobHolder = JobHolder() @@ -45,14 +35,13 @@ internal class CustomCurrencyValidator @Inject constructor( } suspend fun validateForm( - userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Raw, ) = coroutineScope { updateStatus(Status.Validating) - val result = validateTokenFormUseCase( + val result = useCasesFacade.validateTokenFormUseCase( networkId = networkId, formValues = formValues, ) @@ -73,20 +62,19 @@ internal class CustomCurrencyValidator @Inject constructor( launch { when (validatedForm) { is AddCustomTokenForm.Validated.All -> { - findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) + findOrCreateCurrency(networkId, derivationPath, validatedForm) } is AddCustomTokenForm.Validated.ContractAddressOnly -> { - findToken(userWalletId, networkId, derivationPath, validatedForm) + findToken(networkId, derivationPath, validatedForm) } is AddCustomTokenForm.Validated.Empty -> { - createCurrency(userWalletId, networkId, derivationPath, validatedForm = null) + createCurrency(networkId, derivationPath, validatedForm = null) } } }.saveInAndJoin(validateFormJobHolder) } private suspend fun findOrCreateCurrency( - userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.All, @@ -96,14 +84,13 @@ internal class CustomCurrencyValidator @Inject constructor( currentState.prevFoundOrCreatedCurrency.contractAddress == validatedForm.contractAddress ) { // No need to search for token again if contract address is not changed - createCurrency(userWalletId, networkId, derivationPath, validatedForm) + createCurrency(networkId, derivationPath, validatedForm) return } updateStatus(Status.SearchingToken) - val foundToken = findTokenUseCase( - userWalletId = userWalletId, + val foundToken = useCasesFacade.findTokenUseCase( contractAddress = validatedForm.contractAddress, networkId = networkId, derivationPath = derivationPath, @@ -121,22 +108,20 @@ internal class CustomCurrencyValidator @Inject constructor( } if (foundToken != null) { - updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false) + updateStateToValidated(foundToken, fillForm = true, isCustom = false) } else { - createCurrency(userWalletId, networkId, derivationPath, validatedForm) + createCurrency(networkId, derivationPath, validatedForm) } } private suspend fun findToken( - userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.ContractAddressOnly, ) { updateStatus(Status.SearchingToken) - val token = findTokenUseCase( - userWalletId = userWalletId, + val token = useCasesFacade.findTokenUseCase( contractAddress = validatedForm.contractAddress, networkId = networkId, derivationPath = derivationPath, @@ -155,17 +140,15 @@ internal class CustomCurrencyValidator @Inject constructor( return } - updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false) + updateStateToValidated(token, fillForm = true, isCustom = false) } private suspend fun createCurrency( - userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.All?, ) { - val currency = createCryptoCurrencyUseCase( - userWalletId = userWalletId, + val currency = useCasesFacade.createCryptoCurrencyUseCase( networkId = networkId, derivationPath = derivationPath, formValues = validatedForm, @@ -175,20 +158,14 @@ internal class CustomCurrencyValidator @Inject constructor( return } - updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null) + updateStateToValidated(currency, fillForm = false, isCustom = validatedForm != null) } - private suspend fun updateStateToValidated( - userWalletId: UserWalletId, - currency: CryptoCurrency, - fillForm: Boolean, - isCustom: Boolean, - ) { + private suspend fun updateStateToValidated(currency: CryptoCurrency, fillForm: Boolean, isCustom: Boolean) { val currentStatus = state.value.status if (currentStatus is Status.Validated && currentStatus.currency == currency) return - val isNotAdded = checkIsCurrencyNotAddedUseCase( - userWalletId = userWalletId, + val isNotAdded = useCasesFacade.checkIsCurrencyNotAddedUseCase( networkId = currency.network.id, derivationPath = currency.network.derivationPath, contractAddress = when (currency) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt new file mode 100644 index 0000000000..6582d27d32 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -0,0 +1,117 @@ +package com.tangem.features.managetokens.utils.list + +import arrow.core.Either +import arrow.core.NonEmptyList +import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase +import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase +import com.tangem.domain.managetokens.FindTokenUseCase +import com.tangem.domain.managetokens.ValidateTokenFormUseCase +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.managetokens.model.exceptoin.FindTokenException +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase +import com.tangem.features.managetokens.component.AddCustomTokenMode +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("LongParameterList") +internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val validateTokenFormUseCase: ValidateTokenFormUseCase, + private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, + private val findTokenUseCase: FindTokenUseCase, + private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, + @Assisted private val mode: AddCustomTokenMode, +) { + + suspend fun hasMissedDerivationsUseCase(networksWithDerivationPath: Map): Boolean = + when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> hasMissedDerivationsUseCase.invoke( + userWalletId = mode.userWalletId, + networksWithDerivationPath = networksWithDerivationPath, + ) + } + + suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> addCryptoCurrenciesUseCase.invoke( + userWalletId = mode.userWalletId, + currency = currency, + ) + } + + suspend fun derivePublicKeysUseCase(currencies: List): Either = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> derivePublicKeysUseCase.invoke( + userWalletId = mode.userWalletId, + currencies = currencies, + ) + } + + suspend fun checkIsCurrencyNotAddedUseCase( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Either = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> checkIsCurrencyNotAddedUseCase.invoke( + userWalletId = mode.userWalletId, + networkId = networkId, + derivationPath = derivationPath, + contractAddress = contractAddress, + ) + } + + suspend fun createCryptoCurrencyUseCase( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All?, + ): Either = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> createCryptoCurrencyUseCase.invoke( + userWalletId = mode.userWalletId, + networkId = networkId, + derivationPath = derivationPath, + formValues = formValues, + ) + } + + suspend fun findTokenUseCase( + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Either = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> findTokenUseCase.invoke( + userWalletId = mode.userWalletId, + contractAddress = contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ) + } + + suspend fun validateTokenFormUseCase( + networkId: Network.ID, + formValues: AddCustomTokenForm.Raw, + ): Either, AddCustomTokenForm.Validated> = when (mode) { + is AddCustomTokenMode.Account -> TODO("Account") + is AddCustomTokenMode.Wallet -> validateTokenFormUseCase.invoke( + networkId = networkId, + formValues = formValues, + ) + } + + @AssistedFactory + interface Factory { + fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade + } +} \ 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 7664a6ce6e..0964dbe793 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 @@ -9,13 +9,14 @@ 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.* +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.impl.R @@ -24,7 +25,6 @@ import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchListState import com.tangem.pagination.PaginationStatus -import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -42,37 +42,36 @@ import timber.log.Timber @Suppress("LongParameterList", "LargeClass") internal class ManageTokensListManager @AssistedInject constructor( - private val getManagedTokensUseCase: GetManagedTokensUseCase, - private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, - private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, - private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val clipboardManager: ClipboardManager, + manageTokensWarningDelegateFactory: ManageTokensWarningDelegate.Factory, + @Assisted private val useCasesFacade: ManageTokensUseCasesFacade, + @Assisted private val source: ManageTokensSource, + @Assisted private val mode: ManageTokensMode, + @Assisted private val scope: CoroutineScope, @Assisted private val onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {}, ) : ManageTokensUiActions { - private lateinit var scope: CoroutineScope - private lateinit var source: ManageTokensSource - private val jobHolder = JobHolder() private val actionsFlow: MutableSharedFlow = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - private val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + private val state: MutableStateFlow = + MutableStateFlow(ManageTokensListState(mode = mode)) + private val manageTokensWarningDelegate: ManageTokensWarningDelegate = manageTokensWarningDelegateFactory + .create(mode, source, this) private val changedCurrenciesManager = ChangedCurrenciesManager() private val uiManager = ManageTokensUiManager( state = state, - messageSender = messageSender, + manageTokensWarningDelegate = manageTokensWarningDelegate, dispatchers = dispatchers, actions = this, - scopeProvider = Provider { scope }, - sourceProvider = Provider { source }, + scope = scope, ) val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd.asStateFlow() @@ -84,62 +83,61 @@ internal class ManageTokensListManager @AssistedInject constructor( .distinctUntilChanged() val uiItems: Flow> = uiManager.items - suspend fun launchPagination(source: ManageTokensSource, userWalletId: UserWalletId?) = coroutineScope { - scope = this - this@ManageTokensListManager.source = source - - val batchFlow = getManagedTokensUseCase( + suspend fun launchPagination() = coroutineScope { + val loadUserTokensFromRemote = when (mode) { + is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING + is ManageTokensMode.Account, + ManageTokensMode.None, + -> false + } + val batchFlow = useCasesFacade.getManagedTokensUseCase( context = ManageTokensListBatchingContext( actionsFlow = actionsFlow, coroutineScope = this, ), // only for onboarding case, change carefully and check repository implementation - loadUserTokensFromRemote = userWalletId != null && source == ManageTokensSource.ONBOARDING, + loadUserTokensFromRemote = loadUserTokensFromRemote, ) batchFlow.state - .onEach { state -> updateState(state, userWalletId) } + .onEach { state -> updateState(state) } .flowOn(dispatchers.default) .launchIn(scope = this) .saveIn(jobHolder) // Initial load - reload(userWalletId) + reload() } - suspend fun reload(userWalletId: UserWalletId?) { - state.value = ManageTokensListState() + suspend fun reload() { + state.value = ManageTokensListState(mode = mode) actionsFlow.emit( BatchAction.Reload( - requestParams = ManageTokensListConfig(userWalletId, searchText = null), + requestParams = useCasesFacade.manageTokensListConfig(searchText = null), ), ) } - suspend fun loadMore(userWalletId: UserWalletId?, query: String) { + suspend fun loadMore(query: String) { actionsFlow.emit( BatchAction.LoadMore( - requestParams = ManageTokensListConfig(userWalletId, query), + requestParams = useCasesFacade.manageTokensListConfig(query), ), ) } - suspend fun search(userWalletId: UserWalletId?, query: String) { - state.value = ManageTokensListState(searchQuery = query) + suspend fun search(query: String) { + state.value = ManageTokensListState(mode = mode, searchQuery = query) actionsFlow.emit( BatchAction.Reload( - requestParams = ManageTokensListConfig( - userWalletId = userWalletId, + requestParams = useCasesFacade.manageTokensListConfig( searchText = query, ), ), ) } - private fun updateState( - batchListState: BatchListState>, - userWalletId: UserWalletId?, - ) { + private fun updateState(batchListState: BatchListState>) { state.update { state -> state.copy( status = batchListState.status, @@ -154,7 +152,6 @@ internal class ManageTokensListManager @AssistedInject constructor( ) { state.update { state -> state.copy( - userWalletId = userWalletId, currencyBatches = emptyList(), uiBatches = listOf( Batch( @@ -170,7 +167,7 @@ internal class ManageTokensListManager @AssistedInject constructor( scope.launch { state.update { state -> - val newBatches = getDistinctManagedTokensUseCase(batchListState.data) + val newBatches = useCasesFacade.getDistinctManagedTokensUseCase(batchListState.data) val currentBatches = state.currencyBatches // Distinct until changed @@ -181,9 +178,13 @@ internal class ManageTokensListManager @AssistedInject constructor( return@launch } - val canEditItems = userWalletId != null + val canEditItems = when (state.mode) { + is ManageTokensMode.Account, + is ManageTokensMode.Wallet, + -> true + ManageTokensMode.None -> false + } state.copy( - userWalletId = userWalletId, currencyBatches = newBatches, uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), canEditItems = canEditItems, @@ -216,10 +217,10 @@ internal class ManageTokensListManager @AssistedInject constructor( sendSelectCurrencyAnalyticsEvent(currency, isSelected = false) } - override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { + override fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) { scope.launch { - removeCustomCurrencyUseCase.invoke(userWalletId, currency) - .onRight { reload(userWalletId) } + useCasesFacade.removeCustomCurrencyUseCase(currency) + .onRight { reload() } .onLeft { Timber.e(it) } } } @@ -258,9 +259,8 @@ internal class ManageTokensListManager @AssistedInject constructor( actionsFlow.tryEmit(action) } - override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean { - return checkHasLinkedTokensUseCase( - userWalletId = userWalletId, + override suspend fun checkHasLinkedTokens(network: Network): Boolean { + return useCasesFacade.checkHasLinkedTokensUseCase( network = network, tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, @@ -269,7 +269,7 @@ internal class ManageTokensListManager @AssistedInject constructor( it, """ Failed to check linked tokens - |- User wallet ID: $userWalletId + |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), ) @@ -286,18 +286,16 @@ internal class ManageTokensListManager @AssistedInject constructor( } override suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, sourceNetwork: ManagedCryptoCurrency.SourceNetwork, ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, + return useCasesFacade.checkCurrencyUnsupportedUseCase( sourceNetwork = sourceNetwork, ).getOrElse { Timber.e( it, """ Failed to check currency unsupported state - |- User wallet ID: $userWalletId + |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), ) @@ -359,8 +357,7 @@ internal class ManageTokensListManager @AssistedInject constructor( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - val userWalletId = state.value.userWalletId - val unsupportedState = userWalletId?.let { checkCurrencyUnsupportedState(it, source) } + val unsupportedState = checkCurrencyUnsupportedState(source) if (unsupportedState != null) { showUnsupportedWarning(unsupportedState) } else { @@ -368,7 +365,7 @@ internal class ManageTokensListManager @AssistedInject constructor( } } else { if (checkNeedToShowRemoveNetworkWarning(currency, source.network)) { - showRemoveNetworkWarning( + manageTokensWarningDelegate.showRemoveNetworkWarning( currency = currency, network = source.network, isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, @@ -404,67 +401,6 @@ internal class ManageTokensListManager @AssistedInject constructor( 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 } @@ -474,6 +410,12 @@ internal class ManageTokensListManager @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {}): ManageTokensListManager + fun create( + scope: CoroutineScope, + mode: ManageTokensMode, + source: ManageTokensSource, + useCasesFacade: ManageTokensUseCasesFacade, + 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/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt index 42fa36da37..b559a93ae7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils.list import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManageTokensUpdateAction import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction @@ -13,7 +13,7 @@ internal typealias ManageTokensBatchAction = BatchAction = PaginationStatus.None, - val userWalletId: UserWalletId? = null, + val mode: ManageTokensMode, val uiBatches: List>> = mutableListOf(), val currencyBatches: List>> = mutableListOf(), val canEditItems: Boolean = true, 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 8c1873e7da..13a4a44cbd 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 @@ -3,7 +3,6 @@ package com.tangem.features.managetokens.utils.list import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId internal interface ManageTokensUiActions { @@ -13,14 +12,13 @@ internal interface ManageTokensUiActions { fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) - fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean - suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean + suspend fun checkHasLinkedTokens(network: Network): Boolean suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, sourceNetwork: ManagedCryptoCurrency.SourceNetwork, ): CurrencyUnsupportedState? } \ No newline at end of file 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 8c038934c5..23bb3ba720 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,19 +1,10 @@ package com.tangem.features.managetokens.utils.list -import com.tangem.core.decompose.ui.UiMessageSender -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.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.update import com.tangem.pagination.Batch -import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.ImmutableList @@ -29,19 +20,12 @@ import kotlinx.coroutines.launch @Suppress("LongParameterList") internal class ManageTokensUiManager( private val state: MutableStateFlow, - private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, - private val scopeProvider: Provider, - private val sourceProvider: Provider, + private val scope: CoroutineScope, private val actions: ManageTokensUiActions, + private val manageTokensWarningDelegate: ManageTokensWarningDelegate, ) { - private val scope: CoroutineScope - get() = scopeProvider() - - private val source: ManageTokensSource - get() = sourceProvider() - @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = state .mapLatest { state -> @@ -109,75 +93,11 @@ internal class ManageTokensUiManager( } private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) { - showRemoveNetworkWarning( + manageTokensWarningDelegate.showRemoveNetworkWarning( currency = currency, network = currency.network, isCoin = currency is ManagedCryptoCurrency.Custom.Coin, - onConfirm = { - val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" } - actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency) - }, + onConfirm = { actions.removeCustomCurrency(currency = currency) }, ) } - - 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 { - actions.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) - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt new file mode 100644 index 0000000000..6d246fd5e0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -0,0 +1,108 @@ +package com.tangem.features.managetokens.utils.list + +import arrow.core.Either +import arrow.core.left +import com.tangem.domain.managetokens.* +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase +import com.tangem.features.managetokens.component.ManageTokensMode +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("LongParameterList") +internal class ManageTokensUseCasesFacade @AssistedInject constructor( + val getManagedTokensUseCase: GetManagedTokensUseCase, + val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, + private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, + private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, + private val saveManagedTokensUseCase: SaveManagedTokensUseCase, + @Assisted private val mode: ManageTokensMode, +) { + + private val nonePortfolioError: IllegalStateException + get() = IllegalStateException("Unsupported") + + fun manageTokensListConfig(searchText: String?): ManageTokensListConfig { + val userWalletId: UserWalletId? = when (mode) { + is ManageTokensMode.Account -> TODO("Account") + ManageTokensMode.None -> null + is ManageTokensMode.Wallet -> mode.userWalletId + } + return ManageTokensListConfig(userWalletId, searchText) + } + + suspend fun removeCustomCurrencyUseCase(customCurrency: ManagedCryptoCurrency.Custom): Either { + return when (mode) { + is ManageTokensMode.Account -> TODO("Account") + is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke( + userWalletId = mode.userWalletId, + customCurrency = customCurrency, + ) + ManageTokensMode.None -> nonePortfolioError.left() + } + } + + suspend fun checkHasLinkedTokensUseCase( + network: Network, + tempAddedTokens: Map>, + tempRemovedTokens: Map>, + ): Either { + return when (mode) { + is ManageTokensMode.Account -> TODO("Account") + is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke( + userWalletId = mode.userWalletId, + network = network, + tempAddedTokens = tempAddedTokens, + tempRemovedTokens = tempRemovedTokens, + ) + ManageTokensMode.None -> nonePortfolioError.left() + } + } + + suspend fun checkCurrencyUnsupportedUseCase( + sourceNetwork: ManagedCryptoCurrency.SourceNetwork, + ): Either { + return when (mode) { + is ManageTokensMode.Account -> TODO("Account") + is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke( + userWalletId = mode.userWalletId, + sourceNetwork = sourceNetwork, + ) + ManageTokensMode.None -> nonePortfolioError.left() + } + } + + suspend fun hasMissedDerivationsUseCase(network: Map): Boolean = when (mode) { + is ManageTokensMode.Account -> TODO("Account") + is ManageTokensMode.Wallet -> hasMissedDerivationsUseCase.invoke( + userWalletId = mode.userWalletId, + networksWithDerivationPath = network, + ) + ManageTokensMode.None -> false + } + + suspend fun saveManagedTokensUseCase( + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ): Either = when (mode) { + is ManageTokensMode.Account -> TODO("Account") + is ManageTokensMode.Wallet -> saveManagedTokensUseCase.invoke( + userWalletId = mode.userWalletId, + currenciesToAdd = currenciesToAdd, + currenciesToRemove = currenciesToRemove, + ) + ManageTokensMode.None -> nonePortfolioError.left() + } + + @AssistedFactory + interface Factory { + fun create(mode: ManageTokensMode): ManageTokensUseCasesFacade + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt new file mode 100644 index 0000000000..037e69b449 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt @@ -0,0 +1,98 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.core.decompose.ui.UiMessageSender +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.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.features.managetokens.component.ManageTokensMode +import com.tangem.features.managetokens.component.ManageTokensSource +import com.tangem.features.managetokens.impl.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class ManageTokensWarningDelegate @AssistedInject constructor( + private val messageSender: UiMessageSender, + @Assisted private val mode: ManageTokensMode, + @Assisted private val source: ManageTokensSource, + @Assisted private val uiActions: ManageTokensUiActions, +) { + + suspend fun showRemoveNetworkWarning( + currency: ManagedCryptoCurrency, + network: Network, + isCoin: Boolean, + onConfirm: () -> Unit, + ) { + val isNonePortfolio = when (mode) { + ManageTokensMode.None -> true + is ManageTokensMode.Account, + is ManageTokensMode.Wallet, + -> false + } + val hasLinkedTokens = if (isNonePortfolio || !isCoin) { + false + } else { + uiActions.checkHasLinkedTokens(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) + } + + @AssistedFactory + interface Factory { + fun create( + mode: ManageTokensMode, + source: ManageTokensSource, + uiActions: ManageTokensUiActions, + ): ManageTokensWarningDelegate + } +} \ No newline at end of file From 513e0a4d7cb9722143eb0ccfc11605b62a5b23fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 17:39:17 +0400 Subject: [PATCH 33/48] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 5 +- core/config-toggles/build.gradle.kts | 2 +- .../blockchain/ExcludedBlockchainsManager.kt | 2 - .../impl/DefaultExcludedBlockchainsManager.kt | 80 ------- .../impl/DevExcludedBlockchainsManager.kt | 66 ++++++ .../impl/ProdExcludedBlockchainsManager.kt | 27 +++ .../di/ExcludedBlockchainsManagerModule.kt | 33 ++- .../di/FeatureTogglesManagerModule.kt | 7 +- .../feature/impl/DevFeatureTogglesManager.kt | 17 +- .../storage/FeatureTogglesLocalStorage.kt | 31 --- .../storage/LocalTogglesStorage.kt | 32 ++- .../core/configtoggle/utils/CollectionExt.kt | 10 - .../core/configtoggle/utils/StringLogExt.kt | 16 ++ .../impl/DevExcludedBlockchainsManagerTest.kt | 203 ++++++++++++++++++ .../ProdExcludedBlockchainsManagerTest.kt | 98 +++++++++ .../manager/DevFeatureTogglesManagerTest.kt | 4 +- .../storage/LocalTogglesStorageTest.kt | 110 ---------- .../local/preferences/PreferencesKeys.kt | 2 - .../ExcludedBlockchainsViewModel.kt | 5 +- .../utils/ExcludedBlockchains.kt | 4 - 20 files changed, 460 insertions(+), 294 deletions(-) delete mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt create mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt create mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt delete mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt create mode 100644 core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/StringLogExt.kt create mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt create mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt delete mode 100644 core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index a413d65b34..ebad984a1a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -293,16 +293,13 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat Timber.i("APP STARTED") if (BuildConfig.TESTER_MENU_ENABLED) { Timber.i(featureTogglesManager.toString()) + Timber.i(excludedBlockchainsManager.toString()) } foregroundActivityObserver = ForegroundActivityObserver() registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. runBlocking { - awaitAll( - async { excludedBlockchainsManager.init() }, - ) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } diff --git a/core/config-toggles/build.gradle.kts b/core/config-toggles/build.gradle.kts index 368af3bc77..9b23b82a3c 100644 --- a/core/config-toggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -24,7 +24,7 @@ android { } tasks.named("preBuild") { - dependsOn(generateFeatureToggles /*generateExcludedBlockchainToggles*/) + dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles) } tasks.withType().configureEach { diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt index f5db3637b9..4ff86959b7 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt @@ -3,6 +3,4 @@ package com.tangem.core.configtoggle.blockchain interface ExcludedBlockchainsManager { val excludedBlockchainsIds: Set - - suspend fun init() } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt deleted file mode 100644 index 0a260230e9..0000000000 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.core.configtoggle.blockchain.impl - -import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager -import com.tangem.core.configtoggle.storage.TogglesStorage -import com.tangem.core.configtoggle.utils.associateToggles -import com.tangem.core.configtoggle.version.VersionProvider -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMapSync -import com.tangem.datasource.local.preferences.utils.storeObjectMap - -internal class DefaultExcludedBlockchainsManager( - private val localTogglesStorage: TogglesStorage, - private val appPreferencesStore: AppPreferencesStore, - private val versionProvider: VersionProvider, -) : MutableExcludedBlockchainsManager { - - private var isInitialized: Boolean = false - - private lateinit var currentExcludedBlockchains: MutableMap - private lateinit var localExcludedBlockchains: Map - - override val excludedBlockchainsIds: Set - get() { - if (!isInitialized) error("ExcludedBlockchainsManager is not initialized") - - return currentExcludedBlockchains - .filterValues { it } - .keys - } - - override suspend fun init() { - localTogglesStorage.populate(path = "configs/excluded_blockchains_config") - - val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync( - key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, - ) - - localExcludedBlockchains = localTogglesStorage.toggles - .associateToggles(currentVersion = versionProvider.get().orEmpty()) - .mapValues { (_, isIncluded) -> !isIncluded } - - currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys) - .fold(mutableMapOf()) { acc, blockchainId -> - val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId] - - requireNotNull(isExcluded) { - "Unable to find $blockchainId in local or stored excluded blockchains" - } - - acc[blockchainId] = isExcluded - acc - } - - isInitialized = true - } - - override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) { - currentExcludedBlockchains[mainnetId] = isExcluded - - storeCurrent() - } - - override fun isMatchLocalConfig(): Boolean { - return currentExcludedBlockchains == localExcludedBlockchains - } - - override suspend fun recoverLocalConfig() { - currentExcludedBlockchains = localExcludedBlockchains.toMutableMap() - - storeCurrent() - } - - private suspend fun storeCurrent() { - appPreferencesStore.storeObjectMap( - key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, - value = currentExcludedBlockchains, - ) - } -} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt new file mode 100644 index 0000000000..6f6645c865 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt @@ -0,0 +1,66 @@ +package com.tangem.core.configtoggle.blockchain.impl + +import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager +import com.tangem.core.configtoggle.storage.LocalTogglesStorage +import com.tangem.core.configtoggle.utils.defineTogglesAvailability +import com.tangem.core.configtoggle.utils.toTableString +import com.tangem.core.configtoggle.version.VersionProvider +import kotlinx.coroutines.runBlocking +import kotlin.properties.Delegates + +/** + * [MutableExcludedBlockchainsManager] implementation in dev or mocked build + * + * @property versionProvider application version provider + * @property localTogglesStorage local storage for blockchain toggles + */ +internal class DevExcludedBlockchainsManager( + private val versionProvider: VersionProvider, + private val localTogglesStorage: LocalTogglesStorage, +) : MutableExcludedBlockchainsManager { + + private val fileBlockchainToggles: Map = getFileBlockchainToggles() + private var blockchainTogglesMap: MutableMap by Delegates.notNull() + + override val excludedBlockchainsIds: Set + get() = blockchainTogglesMap.filterValues { !it }.keys + + init { + val savedExcludedBlockchains = runBlocking { localTogglesStorage.getSyncOrEmpty() } + + blockchainTogglesMap = fileBlockchainToggles + .mapValues { (blockchainId, isEnabled) -> + savedExcludedBlockchains[blockchainId] ?: isEnabled + } + .toMutableMap() + } + + override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) { + blockchainTogglesMap[mainnetId] = isExcluded + + localTogglesStorage.store(blockchainTogglesMap) + } + + override fun isMatchLocalConfig(): Boolean { + return blockchainTogglesMap == fileBlockchainToggles + } + + override suspend fun recoverLocalConfig() { + blockchainTogglesMap = fileBlockchainToggles.toMutableMap() + + localTogglesStorage.store(blockchainTogglesMap) + } + + override fun toString(): String { + return blockchainTogglesMap + .filterKeys { it.isNotEmpty() } + .toTableString(tableName = this@DevExcludedBlockchainsManager::class.java.simpleName) + } + + private fun getFileBlockchainToggles(): Map { + val appVersion = versionProvider.get() + + return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion) + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt new file mode 100644 index 0000000000..bc9eb689c4 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt @@ -0,0 +1,27 @@ +package com.tangem.core.configtoggle.blockchain.impl + +import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.core.configtoggle.utils.defineTogglesAvailability +import com.tangem.core.configtoggle.version.VersionProvider + +/** + * [ExcludedBlockchainsManager] implementation in PROD build + * + * @property versionProvider application version provider + */ +internal class ProdExcludedBlockchainsManager( + private val versionProvider: VersionProvider, +) : ExcludedBlockchainsManager { + + override val excludedBlockchainsIds: Set = getBlockchainToggles() + + private fun getBlockchainToggles(): Set { + val appVersion = versionProvider.get() + + return ExcludedBlockchainToggles.values + .defineTogglesAvailability(appVersion = appVersion) + .filterValues { !it } + .keys + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt index 2e4fcddac5..14d9514eae 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt @@ -3,11 +3,10 @@ package com.tangem.core.configtoggle.di import android.content.Context import com.tangem.core.configtoggle.BuildConfig import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager -import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager -import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.DefaultVersionProvider -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides @@ -24,26 +23,20 @@ internal object ExcludedBlockchainsManagerModule { @Singleton fun provideExcludedBlockchainsManager( @ApplicationContext context: Context, - assetLoader: AssetLoader, appPreferencesStore: AppPreferencesStore, ): ExcludedBlockchainsManager { - val localTogglesStorage = LocalTogglesStorage(assetLoader) val versionProvider = DefaultVersionProvider(context) - return DefaultExcludedBlockchainsManager( - localTogglesStorage, - appPreferencesStore, - versionProvider, - ) - } - - @Provides - @Singleton - fun provideMutableExcludedBlockchainsManager( - manager: ExcludedBlockchainsManager, - ): MutableExcludedBlockchainsManager? { - if (!BuildConfig.TESTER_MENU_ENABLED) return null - - return manager as MutableExcludedBlockchainsManager + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevExcludedBlockchainsManager( + versionProvider = versionProvider, + localTogglesStorage = LocalTogglesStorage( + appPreferencesStore = appPreferencesStore, + preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY, + ), + ) + } else { + ProdExcludedBlockchainsManager(versionProvider = versionProvider) + } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 79ad0221fc..877f25842d 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -5,7 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager -import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage +import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.DefaultVersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module @@ -30,7 +30,10 @@ internal object FeatureTogglesManagerModule { return if (BuildConfig.TESTER_MENU_ENABLED) { DevFeatureTogglesManager( versionProvider = versionProvider, - featureTogglesLocalStorage = FeatureTogglesLocalStorage(appPreferencesStore), + featureTogglesLocalStorage = LocalTogglesStorage( + appPreferencesStore = appPreferencesStore, + preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY, + ), ) } else { ProdFeatureTogglesManager(versionProvider = versionProvider) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 6677cae6d0..84dd090158 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -3,11 +3,11 @@ package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager -import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage +import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.utils.defineTogglesAvailability +import com.tangem.core.configtoggle.utils.toTableString import com.tangem.core.configtoggle.version.VersionProvider import kotlinx.coroutines.runBlocking -import java.util.Locale import kotlin.properties.Delegates /** @@ -18,7 +18,7 @@ import kotlin.properties.Delegates */ internal class DevFeatureTogglesManager( private val versionProvider: VersionProvider, - private val featureTogglesLocalStorage: FeatureTogglesLocalStorage, + private val featureTogglesLocalStorage: LocalTogglesStorage, ) : MutableFeatureTogglesManager { private var fileFeatureTogglesMap: Map = getFileFeatureToggles() @@ -52,16 +52,7 @@ internal class DevFeatureTogglesManager( } override fun toString(): String { - return buildString { - append("DevFeatureTogglesManager:\n") - append("|------------------------------------------|-----------|\n") - append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled")) - append("|------------------------------------------|-----------|\n") - featureTogglesMap.entries.forEachIndexed { index, (name, isEnabled) -> - append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled)) - } - append("|------------------------------------------|-----------|") - } + return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName) } private fun getFileFeatureToggles(): Map { diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt deleted file mode 100644 index 7ab7d42891..0000000000 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/FeatureTogglesLocalStorage.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.core.configtoggle.storage - -import androidx.datastore.preferences.core.stringPreferencesKey -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.utils.getObjectMapSync -import com.tangem.datasource.local.preferences.utils.storeObjectMap - -/** - * Local storage for feature toggles - * - * @property appPreferencesStore app preferences store - * -[REDACTED_AUTHOR] - */ -internal class FeatureTogglesLocalStorage( - private val appPreferencesStore: AppPreferencesStore, -) { - - suspend fun getSyncOrEmpty(): Map { - return appPreferencesStore.getObjectMapSync(key = FEATURE_TOGGLES_KEY) - } - - suspend fun store(value: Map) { - appPreferencesStore.storeObjectMap(key = FEATURE_TOGGLES_KEY, value = value) - } - - private companion object { - - val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } - } -} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt index 59086d9658..fe575dc0ef 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt @@ -1,24 +1,34 @@ package com.tangem.core.configtoggle.storage -import com.tangem.datasource.asset.loader.AssetLoader -import kotlin.properties.Delegates +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.storeObjectMap /** - * Storage implementation for storing local feature toggles. - * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. + * Local storage for toggles * - * @property assetLoader asset loader + * @property appPreferencesStore app preferences store + * @property preferencesKey preferences key * [REDACTED_AUTHOR] */ internal class LocalTogglesStorage( - private val assetLoader: AssetLoader, -) : TogglesStorage { + private val appPreferencesStore: AppPreferencesStore, + private val preferencesKey: Preferences.Key, +) { - override var toggles: List by Delegates.notNull() - private set + suspend fun getSyncOrEmpty(): Map { + return appPreferencesStore.getObjectMapSync(key = preferencesKey) + } - override suspend fun populate(path: String) { - toggles = assetLoader.loadList(path) + suspend fun store(value: Map) { + appPreferencesStore.storeObjectMap(key = preferencesKey, value = value) + } + + companion object { + val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } + val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt index d3e54bcac1..052ec8ab62 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt @@ -1,17 +1,7 @@ package com.tangem.core.configtoggle.utils -import com.tangem.core.configtoggle.storage.ConfigToggle import com.tangem.core.configtoggle.version.VersionAvailabilityContract -internal fun List.associateToggles(currentVersion: String): Map { - return associate { localToggle -> - Pair( - first = localToggle.name, - second = VersionAvailabilityContract(currentVersion, localToggle.version), - ) - } -} - internal fun Map.defineTogglesAvailability(appVersion: String?): Map { return if (appVersion == null) { mapValues { false } diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/StringLogExt.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/StringLogExt.kt new file mode 100644 index 0000000000..85293bf111 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/StringLogExt.kt @@ -0,0 +1,16 @@ +package com.tangem.core.configtoggle.utils + +import java.util.Locale + +internal fun Map.toTableString(tableName: String): String { + return buildString { + append("$tableName:\n") + append("|------------------------------------------|-----------|\n") + append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled")) + append("|------------------------------------------|-----------|\n") + entries.forEachIndexed { index, (name, isEnabled) -> + append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled)) + } + append("|------------------------------------------|-----------|") + } +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt new file mode 100644 index 0000000000..18eb584b52 --- /dev/null +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt @@ -0,0 +1,203 @@ +package com.tangem.core.configtoggle.blockchain.impl + +import com.google.common.truth.Truth +import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.storage.LocalTogglesStorage +import com.tangem.core.configtoggle.version.VersionProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DevExcludedBlockchainsManagerTest { + + private val versionProvider = mockk() + private val localTogglesStorage = mockk(relaxUnitFun = true) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @BeforeAll + fun setupAll() { + val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0") + mockkObject(ExcludedBlockchainToggles) + every { ExcludedBlockchainToggles.values } returns toggles + } + + @AfterAll + fun tearDownAll() { + unmockkObject(ExcludedBlockchainToggles) + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, localTogglesStorage) + } + + @Test + fun `successfully initialize manager`() = runTest { + // Arrange + val appVersion = "1.0.0" + val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true) + every { versionProvider.get() } returns appVersion + coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles + + // Act + val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + + // Assert + val expected = setOf("CHAIN_1") + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerifyOrder { + versionProvider.get() + localTogglesStorage.getSyncOrEmpty() + } + } + + @Test + fun `successfully initialize manager if versionProvider returns null`() = runTest { + // Arrange + every { versionProvider.get() } returns null + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + + // Act + val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + + // Assert + Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2") + + coVerifyOrder { + versionProvider.get() + localTogglesStorage.getSyncOrEmpty() + } + } + + @Test + fun `successfully initialize manager if storage returns empty map`() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + + // Act + val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + + // Assert + Truth.assertThat(actual).containsExactly("CHAIN_2") + + coVerifyOrder { + versionProvider.get() + localTogglesStorage.getSyncOrEmpty() + } + } + + @Test + fun `failure initialize manager if storage throws exception`() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + val exception = Exception("Test exception") + coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception + + // Act + val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) } + .exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { + versionProvider.get() + localTogglesStorage.getSyncOrEmpty() + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ExcludeBlockchain { + + @Test + fun excludeBlockchain_changesStatusAndSaves() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + + val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) + manager.excludeBlockchain("CHAIN_1", false) + + // Act + val actual = manager.excludedBlockchainsIds + + // Assert + Truth.assertThat(actual).contains("CHAIN_1") + coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsMatchLocalConfig { + + @Test + fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + + val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) + + // Act + val actual = manager.isMatchLocalConfig() + + // Assert + Truth.assertThat(actual).isTrue() + } + + @Test + fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + + val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) + manager.excludeBlockchain("CHAIN_1", false) + + // Act + val actual = manager.isMatchLocalConfig() + + // Assert + Truth.assertThat(actual).isFalse() + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class RecoverLocalConfig { + + @Test + fun recoverLocalConfig_resetsToFileAndSaves() = runTest { + // Arrange + every { versionProvider.get() } returns "1.0.0" + + val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0") + mockkObject(ExcludedBlockchainToggles) + every { ExcludedBlockchainToggles.values } returns toggles + + coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true) + + val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) + manager.recoverLocalConfig() + + // Act + val actual = manager.excludedBlockchainsIds + + // Assert + Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2") + + unmockkObject(ExcludedBlockchainToggles) + clearMocks(versionProvider, localTogglesStorage) + } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt new file mode 100644 index 0000000000..68fdf3c8b6 --- /dev/null +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt @@ -0,0 +1,98 @@ +package com.tangem.core.configtoggle.blockchain.impl + +import com.google.common.truth.Truth +import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.version.VersionProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ProdExcludedBlockchainsManagerTest { + + private val versionProvider = mockk() + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @BeforeAll + fun setupAll() { + val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0") + mockkObject(ExcludedBlockchainToggles) + every { ExcludedBlockchainToggles.values } returns toggles + } + + @AfterAll + fun tearDownAll() { + unmockkObject(ExcludedBlockchainToggles) + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider) + } + + @Test + fun `successfully initialize excluded blockchains`() = runTest { + // Arrange + val appVersion = "1.0.0" + every { versionProvider.get() } returns appVersion + + // Act + val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + + // Assert + val expected = setOf("CHAIN_2") + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `all blockchains excluded if versionProvider returns null`() = runTest { + // Arrange + every { versionProvider.get() } returns null + + // Act + val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + + // Assert + val expected = setOf("CHAIN_1", "CHAIN_2") + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `all blockchains excluded if versionProvider returns empty string`() = runTest { + // Arrange + every { versionProvider.get() } returns "" + + // Act + val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + + // Assert + val expected = setOf("CHAIN_1", "CHAIN_2") + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerifyOrder { versionProvider.get() } + } + + @Test + fun `failure initialize if versionProvider throws exception`() = runTest { + // Arrange + val exception = Exception("Test exception") + every { versionProvider.get() } throws exception + + // Act + val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!! + + // Assert + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) + + coVerifyOrder { versionProvider.get() } + } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt index e91cc5d705..4970a9c1cf 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt @@ -5,7 +5,7 @@ import com.tangem.common.test.utils.ProvideTestModels import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel -import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage +import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.VersionProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -19,7 +19,7 @@ import org.junit.jupiter.params.ParameterizedTest internal class DevFeatureTogglesManagerTest { private val versionProvider = mockk() - private val featureTogglesLocalStorage = mockk(relaxUnitFun = true) + private val featureTogglesLocalStorage = mockk(relaxUnitFun = true) @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt deleted file mode 100644 index b2f5bc4be7..0000000000 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.core.configtoggle.storage - -import android.annotation.SuppressLint -import com.google.common.truth.Truth -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants -import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.asset.reader.AssetReader -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.io.IOException - -/** -[REDACTED_AUTHOR] - */ -@SuppressLint("CheckResult") -internal class LocalTogglesStorageTest { - - private val assetReader = mockk() - private val moshi = mockk() - private val jsonAdapter = mockk>>() - - // Impossible to mockk AssetLoader because it implement inline functions - private val assetLoader = AssetLoader( - assetReader = assetReader, - moshi = moshi, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - private val storage = LocalTogglesStorage(assetLoader) - - @Test - fun `successfully initialize storage`() = runTest { - everyReadingJson() returns json - everyCreatingMoshiAdapter() returns jsonAdapter - everyMappingJson() returns featureToggles - - storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - - coVerifyOrder { - assetReader.read(CONFIG_FILE_NAME) - jsonAdapter.fromJson(json) - } - - Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles) - } - - @Test - fun `failure initialize storage if assetReader throws exception`() = runTest { - everyReadingJson() returns json - everyCreatingMoshiAdapter() returns jsonAdapter - everyMappingJson() throws IOException() - - storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - - coVerifyOrder { - assetReader.read(CONFIG_FILE_NAME) - jsonAdapter.fromJson(json) - } - - Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList()) - } - - @Test - fun `failure initialize storage if jsonAdapter throws exception`() = runTest { - everyReadingJson() throws IOException() - - storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) - - coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) } - verifyAll(inverse = true) { jsonAdapter.fromJson(any()) } - - Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList()) - } - - private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) } - - private fun everyCreatingMoshiAdapter() = every { - val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java) - moshi.adapter>(types) - } - - private fun everyMappingJson() = every { jsonAdapter.fromJson(json) } - - private companion object { - const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json" - - val json = """ - [ - { - "name": "INACTIVE_TEST_FEATURE_ENABLED", - "version": "undefined" - }, - { - "name": "ACTIVE2_TEST_FEATURE_ENABLED", - "version": "1.0.0" - } - ] - """.trimIndent() - - val featureToggles = listOf( - ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 162d75a40f..8644fb72c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -56,8 +56,6 @@ object PreferencesKeys { val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } - val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") } - val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt index 213bfbc246..3c2015978a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tester.presentation.excludedblockchains import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchain.common.Blockchain +import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.components.fields.entity.SearchBarUM @@ -25,11 +26,11 @@ import javax.inject.Inject @HiltViewModel internal class ExcludedBlockchainsViewModel @Inject constructor( private val appVersionProvider: AppVersionProvider, - excludedBlockchainsManager: MutableExcludedBlockchainsManager?, + excludedBlockchainsManager: ExcludedBlockchainsManager, ) : ViewModel() { private val excludedBlockchainsManager: MutableExcludedBlockchainsManager = - requireNotNull(excludedBlockchainsManager) { + requireNotNull(excludedBlockchainsManager as? MutableExcludedBlockchainsManager) { "Mutable excluded blockchains manager can't be null when tester actions is available" } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ExcludedBlockchains.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ExcludedBlockchains.kt index 6e2cb4877d..e2d3d72ea2 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ExcludedBlockchains.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ExcludedBlockchains.kt @@ -28,10 +28,6 @@ class ExcludedBlockchains @Inject internal constructor( excludedBlockchainsManager = object : ExcludedBlockchainsManager { override val excludedBlockchainsIds: Set = emptySet() - - override suspend fun init() { - /* no-op */ - } }, ) From 17d7fb3c0209728aa99cc06860926323ad7cc864 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 19:21:37 +0300 Subject: [PATCH 34/48] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 4 +- .../java/com/tangem/tap/TangemApplication.kt | 8 +-- .../tap/di/domain/FeedbackDomainModule.kt | 6 +- .../domain/scanCard/LegacyScanProcessor.kt | 2 +- .../ui/dialogs/WalletActivationErrorDialog.kt | 2 +- .../tap/proxy/redux/DaggerGraphState.kt | 4 +- .../com/tangem/common/routing/AppRoute.kt | 7 +- data/feedback/build.gradle.kts | 3 +- .../feedback/DefaultFeedbackRepository.kt | 38 ++++++++++- .../feedback/converters/CardInfoConverter.kt | 41 ------------ .../converters/WalletMetaInfoConverter.kt | 66 +++++++++++++++++++ .../tangem/data/feedback/di/FeedbackModule.kt | 6 ++ .../tangem/domain/feedback/models/CardInfo.kt | 21 ------ .../feedback/models/FeedbackEmailType.kt | 26 ++++---- .../domain/feedback/models/WalletMetaInfo.kt | 22 +++++++ .../domain/feedback/FeedbackDataBuilder.kt | 17 ++--- .../domain/feedback/GetCardInfoUseCase.kt | 23 ------- .../feedback/GetWalletMetaInfoUseCase.kt | 28 ++++++++ .../feedback/SendFeedbackEmailUseCase.kt | 4 +- .../feedback/repository/FeedbackRepository.kt | 4 +- .../utils/EmailMessageBodyResolver.kt | 49 +++++++------- .../feedback/utils/EmailSubjectResolver.kt | 2 +- .../features/details/model/DetailsModel.kt | 65 ++++++++---------- .../model/MultiWalletCreateWalletModel.kt | 7 +- .../model/MultiWalletFinalizeModel.kt | 7 +- .../model/MultiWalletSeedPhraseModel.kt | 7 +- .../impl/DefaultOnboardingStepperComponent.kt | 6 +- .../v2/send/confirm/model/SendConfirmModel.kt | 16 ++--- .../features/send/v2/send/model/SendModel.kt | 15 ++--- .../confirm/model/NFTSendConfirmModel.kt | 16 ++--- .../send/v2/sendnft/model/NFTSendModel.kt | 15 ++--- .../impl/presentation/model/StakingModel.kt | 16 ++--- .../swap/v2/impl/common/SwapAlertFactory.kt | 14 ++-- .../tangem/feature/swap/model/SwapModel.kt | 13 ++-- .../wallet/model/intents/VisaWalletIntents.kt | 8 +-- .../intents/WalletWarningsClickIntents.kt | 24 ++----- 36 files changed, 311 insertions(+), 301 deletions(-) delete mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt create mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/converters/WalletMetaInfoConverter.kt delete mode 100644 domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/CardInfo.kt create mode 100644 domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/WalletMetaInfo.kt delete mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/GetWalletMetaInfoUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index aa3d4f2a9a..ce53e6dc05 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -29,7 +29,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.core.wallets.UserWalletsListRepository -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -107,7 +107,7 @@ interface ApplicationEntryPoint { fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase - fun getGetCardInfoUseCase(): GetCardInfoUseCase + fun getWalletMetaInfoUseCase(): GetWalletMetaInfoUseCase fun getUrlOpener(): UrlOpener diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ebad984a1a..4c35b0ee8f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -47,7 +47,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -166,8 +166,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase get() = entryPoint.getSendFeedbackEmailUseCase() - private val getCardInfoUseCase: GetCardInfoUseCase - get() = entryPoint.getGetCardInfoUseCase() + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase + get() = entryPoint.getWalletMetaInfoUseCase() private val urlOpener get() = entryPoint.getUrlOpener() @@ -359,7 +359,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, - getCardInfoUseCase = getCardInfoUseCase, + getWalletMetaInfoUseCase = getWalletMetaInfoUseCase, issuersConfigStorage = issuersConfigStorage, urlOpener = urlOpener, shareManager = shareManager, diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index b905aa7edc..48bfb0b921 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di.domain import android.content.Context -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.repository.FeedbackRepository @@ -18,8 +18,8 @@ internal object FeedbackDomainModule { @Provides @Singleton - fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase { - return GetCardInfoUseCase(feedbackRepository = feedbackRepository) + fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetWalletMetaInfoUseCase { + return GetWalletMetaInfoUseCase(feedbackRepository = feedbackRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 386cddb495..ea780d4fa5 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -265,7 +265,7 @@ internal class LegacyScanProcessor @Inject constructor( onOk = { mainScope.launch { onSuccess() } }, onSupportClick = { val cardInfo = - store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull() + store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() ?: error("CardInfo must be not null") scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt index d5851ab0ed..880f9177a0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -30,7 +30,7 @@ object WalletActivationErrorDialog { val scanResponse = store.state.globalState.scanResponse ?: error("ScanResponse must be not null") - val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull() + val cardInfo = store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() ?: error("CardInfo must be not null") scope.launch { diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index e0b2794648..f7e6c83846 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -22,7 +22,7 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.core.wallets.UserWalletsListRepository -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -63,7 +63,7 @@ data class DaggerGraphState( val settingsRepository: SettingsRepository? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null, val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null, - val getCardInfoUseCase: GetCardInfoUseCase? = null, + val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase? = null, val issuersConfigStorage: IssuersConfigStorage? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index cfa1070c25..20d91d8960 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -1,14 +1,13 @@ package com.tangem.common.routing import android.os.Bundle -import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -83,8 +82,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Usedesk( - val cardInfo: CardInfo, - ) : AppRoute(path = "/usedesk/${cardInfo.cardId}") + val walletMetaInfo: WalletMetaInfo, + ) : AppRoute(path = "/usedesk/${walletMetaInfo.userWalletId}") @Serializable data class CardSettings( diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 9826df2814..08b86b9db4 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -10,7 +10,6 @@ android { } dependencies { - // region AndroidX libraries implementation(deps.androidx.datastore) // endregion @@ -37,6 +36,8 @@ dependencies { // endregion + // Feature modules + implementation(projects.features.hotWallet.api) implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index c64229e83a..54cb04d95b 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -4,12 +4,14 @@ import android.os.Build import com.tangem.blockchain.common.Blockchain import com.tangem.core.navigation.email.EmailSender import com.tangem.data.feedback.converters.BlockchainInfoConverter -import com.tangem.data.feedback.converters.CardInfoConverter +import com.tangem.data.feedback.converters.WalletMetaInfoConverter import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase @@ -24,14 +26,19 @@ import java.io.File * * @property appLogsStore app logs store * @property userWalletsListManager user wallets list manager + * @property useNewUserWalletsRepository flag to use new user wallets repository + * @property userWalletsListRepository user wallets repository * @property walletManagersStore wallet managers store * @property emailSender email sender * @property appVersionProvider app version provider * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultFeedbackRepository( private val appLogsStore: AppLogsStore, + private val useNewUserWalletsRepository: Boolean, + private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, private val emailSender: EmailSender, @@ -41,12 +48,21 @@ internal class DefaultFeedbackRepository( private val blockchainsErrors = MutableStateFlow>(emptyMap()) - override fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) + override suspend fun getUserWalletMetaInfo(userWalletId: UserWalletId): WalletMetaInfo { + val userWallet = getUserWalletById(userWalletId) + return userWallet?.let { + WalletMetaInfoConverter.convert(it) + } ?: WalletMetaInfo(userWalletId) + } + + override fun getUserWalletMetaInfo(scanResponse: ScanResponse): WalletMetaInfo { + return WalletMetaInfoConverter.convert(value = scanResponse) + } override fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { return UserWalletsInfo( selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated", - totalUserWallets = userWalletsListManager.walletsCount, + totalUserWallets = totalUserWallets(), ) } @@ -108,4 +124,20 @@ internal class DefaultFeedbackRepository( ), ) } + + private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? { + return if (useNewUserWalletsRepository) { + userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId } + } else { + userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId } + } + } + + private fun totalUserWallets(): Int { + return if (useNewUserWalletsRepository) { + userWalletsListRepository.userWallets.value?.size ?: 0 + } else { + userWalletsListManager.walletsCount + } + } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt deleted file mode 100644 index 1b17450f2d..0000000000 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.data.feedback.converters - -import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin -import com.tangem.domain.card.common.TapWorkarounds.isVisa -import com.tangem.domain.card.common.util.getBackupCardsCount -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.utils.converter.Converter - -/** - * Converter from [ScanResponse] to [CardInfo] - * -[REDACTED_AUTHOR] - */ -internal object CardInfoConverter : Converter { - - override fun convert(value: ScanResponse): CardInfo { - return with(value) { - CardInfo( - userWalletId = createUserWalletId(scanResponse = value), - cardId = card.cardId, - cardsCount = value.getBackupCardsCount()?.toString() ?: "0", - firmwareVersion = card.firmwareVersion.stringValue, - cardBlockchain = walletData?.blockchain, - signedHashesList = card.wallets.map { - CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) - }, - isImported = value.card.wallets.any(CardDTO.Wallet::isImported), - isStart2Coin = value.card.isStart2Coin, - isVisa = value.card.isVisa, - ) - } - } - - private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? { - return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build() - } -} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/WalletMetaInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/WalletMetaInfoConverter.kt new file mode 100644 index 0000000000..ceaabe5809 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/WalletMetaInfoConverter.kt @@ -0,0 +1,66 @@ +package com.tangem.data.feedback.converters + +import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.card.common.TapWorkarounds.isVisa +import com.tangem.domain.card.common.util.getBackupCardsCount +import com.tangem.domain.feedback.models.WalletMetaInfo +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] to [WalletMetaInfo] + * +[REDACTED_AUTHOR] + */ +internal object WalletMetaInfoConverter : Converter { + + override fun convert(value: UserWallet): WalletMetaInfo { + return when (value) { + is UserWallet.Cold -> { + WalletMetaInfo( + userWalletId = value.walletId, + cardId = value.scanResponse.card.cardId, + cardsCount = value.getBackupCardsCount()?.toString() ?: "0", + firmwareVersion = value.scanResponse.card.firmwareVersion.stringValue, + cardBlockchain = value.scanResponse.walletData?.blockchain, + signedHashesList = value.scanResponse.card.wallets.map { + WalletMetaInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) + }, + isImported = value.scanResponse.card.wallets.any(CardDTO.Wallet::isImported), + isStart2Coin = value.scanResponse.card.isStart2Coin, + isVisa = value.scanResponse.card.isVisa, + ) + } + is UserWallet.Hot -> { + WalletMetaInfo( + userWalletId = value.walletId, + hotWalletIsBackedUp = value.backedUp, + ) + } + } + } + + fun convert(value: ScanResponse): WalletMetaInfo { + return WalletMetaInfo( + userWalletId = createUserWalletId(value), + cardId = value.card.cardId, + cardsCount = value.getBackupCardsCount()?.toString() ?: "0", + firmwareVersion = value.card.firmwareVersion.stringValue, + cardBlockchain = value.walletData?.blockchain, + signedHashesList = value.card.wallets.map { + WalletMetaInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) + }, + isImported = value.card.wallets.any(CardDTO.Wallet::isImported), + isStart2Coin = value.card.isStart2Coin, + isVisa = value.card.isVisa, + ) + } + + private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? { + return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build() + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index fd2bc36968..067feab7a8 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -6,10 +6,12 @@ import com.tangem.data.feedback.DefaultFeedbackFeatureToggles import com.tangem.data.feedback.DefaultFeedbackRepository import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -26,6 +28,8 @@ internal object FeedbackModule { fun provideFeedbackRepository( appLogsStore: AppLogsStore, userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, walletManagersStore: WalletManagersStore, emailSender: EmailSender, appVersionProvider: AppVersionProvider, @@ -37,6 +41,8 @@ internal object FeedbackModule { walletManagersStore = walletManagersStore, emailSender = emailSender, appVersionProvider = appVersionProvider, + userWalletsListRepository = userWalletsListRepository, + useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, getSelectedWalletUseCase = getSelectedWalletUseCase, ) } diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/CardInfo.kt deleted file mode 100644 index 527416d699..0000000000 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/CardInfo.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.domain.feedback.models - -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -@Serializable -data class CardInfo( - val userWalletId: UserWalletId?, - val cardId: String, - val firmwareVersion: String, - val cardsCount: String, - val cardBlockchain: String?, - val signedHashesList: List, - val isImported: Boolean, - val isStart2Coin: Boolean, - val isVisa: Boolean, -) { - - @Serializable - data class SignedHashes(val curve: String, val total: String?) -} \ No newline at end of file diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 7853427ca5..5649d73a23 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -9,32 +9,32 @@ import com.tangem.domain.visa.model.VisaTxDetails */ sealed interface FeedbackEmailType { - val cardInfo: CardInfo? + val walletMetaInfo: WalletMetaInfo? /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ - data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType + data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType /** User rate the app as "can be better" */ - data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType + data class RateCanBeBetter(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType /** User has problem with scanning */ data object ScanningProblem : FeedbackEmailType { - override val cardInfo: CardInfo? = null + override val walletMetaInfo: WalletMetaInfo? = null } /** User has problem with sending transaction */ - data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType + data class TransactionSendingProblem(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType /** User has problem with staking */ data class StakingProblem( - override val cardInfo: CardInfo, + override val walletMetaInfo: WalletMetaInfo, val validatorName: String?, val transactionTypes: List, val unsignedTransactions: List, ) : FeedbackEmailType data class SwapProblem( - override val cardInfo: CardInfo, + override val walletMetaInfo: WalletMetaInfo, val providerName: String, val txId: String, ) : FeedbackEmailType @@ -46,23 +46,23 @@ sealed interface FeedbackEmailType { * @property currencyName currency name */ data class CurrencyDescriptionError(val currencyId: String, val currencyName: String) : FeedbackEmailType { - override val cardInfo: CardInfo? = null + override val walletMetaInfo: WalletMetaInfo? = null } - data class PreActivatedWallet(override val cardInfo: CardInfo) : FeedbackEmailType + data class PreActivatedWallet(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType data object CardAttestationFailed : FeedbackEmailType { - override val cardInfo: CardInfo? = null + override val walletMetaInfo: WalletMetaInfo? = null } sealed class Visa : FeedbackEmailType { - data class DirectUserRequest(override val cardInfo: CardInfo) : Visa() + data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa() - data class Activation(override val cardInfo: CardInfo) : Visa() + data class Activation(override val walletMetaInfo: WalletMetaInfo) : Visa() data class Dispute( val visaTxDetails: VisaTxDetails, - override val cardInfo: CardInfo, + override val walletMetaInfo: WalletMetaInfo, ) : Visa() } } \ No newline at end of file diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/WalletMetaInfo.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/WalletMetaInfo.kt new file mode 100644 index 0000000000..8b9f46f512 --- /dev/null +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/WalletMetaInfo.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.feedback.models + +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +data class WalletMetaInfo( + val userWalletId: UserWalletId?, + val hotWalletIsBackedUp: Boolean? = null, + val cardId: String? = null, + val firmwareVersion: String? = null, + val cardsCount: String? = null, + val cardBlockchain: String? = null, + val signedHashesList: List? = null, + val isImported: Boolean? = null, + val isStart2Coin: Boolean? = null, + val isVisa: Boolean? = null, +) { + + @Serializable + data class SignedHashes(val curve: String, val total: String?) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 7d0f5aa49f..e5f65f4c77 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -44,13 +44,14 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString()) } - fun addCardInfo(cardInfo: CardInfo) { - builder.appendKeyValue("Card ID", cardInfo.cardId) - builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion) - builder.appendKeyValue("Linked cards count", cardInfo.cardsCount) - builder.appendKeyValue("Has seed phrase", cardInfo.isImported.toString()) - builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) - builder.appendSignedHashes(cardInfo.signedHashesList) + fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) { + builder.appendKeyValue("Mobile Wallet is backed up", walletMetaInfo.hotWalletIsBackedUp?.toString()) + builder.appendKeyValue("Card ID", walletMetaInfo.cardId) + builder.appendKeyValue("Firmware version", walletMetaInfo.firmwareVersion) + builder.appendKeyValue("Linked cards count", walletMetaInfo.cardsCount) + builder.appendKeyValue("Has seed phrase", walletMetaInfo.isImported?.toString()) + builder.appendKeyValue("Card Blockchain", walletMetaInfo.cardBlockchain) + walletMetaInfo.signedHashesList?.let { builder.appendSignedHashes(it) } } fun addBlockchainInfoList(blockchainInfoList: List) { @@ -146,7 +147,7 @@ internal class FeedbackDataBuilder { append("$keyValuePrefix$value\n") } - private fun StringBuilder.appendSignedHashes(signedHashesList: List) { + private fun StringBuilder.appendSignedHashes(signedHashesList: List) { signedHashesList.forEach { appendKeyValue("Signed hashes [${it.curve}]", it.total) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt deleted file mode 100644 index f2ecf40fca..0000000000 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.domain.feedback - -import arrow.core.Either -import arrow.core.Either.Companion.catch -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.models.scan.ScanResponse - -/** - * UseCase for creating 'CardInfo' - * - * @property feedbackRepository feedback repository - * -[REDACTED_AUTHOR] - */ -class GetCardInfoUseCase( - private val feedbackRepository: FeedbackRepository, -) { - - operator fun invoke(scanResponse: ScanResponse): Either = catch { - feedbackRepository.getCardInfo(scanResponse) - } -} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetWalletMetaInfoUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetWalletMetaInfoUseCase.kt new file mode 100644 index 0000000000..a3bd2788d4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetWalletMetaInfoUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.feedback + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.feedback.models.WalletMetaInfo +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWalletId + +/** + * UseCase for creating 'UserWalletMetaInfo' from [UserWalletId] or [ScanResponse] + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class GetWalletMetaInfoUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either = catch { + feedbackRepository.getUserWalletMetaInfo(userWalletId) + } + + operator fun invoke(scanResponse: ScanResponse): Either = catch { + feedbackRepository.getUserWalletMetaInfo(scanResponse) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 86cd5c8945..a8d5177b41 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -37,8 +37,8 @@ class SendFeedbackEmailUseCase( private fun getAddress(type: FeedbackEmailType): String { return when { - type is FeedbackEmailType.Visa || type.cardInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL - type.cardInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL + type is FeedbackEmailType.Visa || type.walletMetaInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL + type.walletMetaInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL else -> TANGEM_SUPPORT_EMAIL } } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 55ee6e8be1..3661f00b48 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -7,7 +7,9 @@ import java.io.File interface FeedbackRepository { - fun getCardInfo(scanResponse: ScanResponse): CardInfo + suspend fun getUserWalletMetaInfo(userWalletId: UserWalletId): WalletMetaInfo + + fun getUserWalletMetaInfo(scanResponse: ScanResponse): WalletMetaInfo fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index b976293f59..5a94a88857 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -1,7 +1,7 @@ package com.tangem.domain.feedback.utils import com.tangem.domain.feedback.FeedbackDataBuilder -import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.visa.model.VisaTxDetails @@ -20,37 +20,40 @@ internal class EmailMessageBodyResolver( /** Resolve email message body by [type] */ suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) { when (type) { - is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo) - is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) - is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) + is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) + is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.walletMetaInfo) + is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.walletMetaInfo) is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type) is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type) is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type) - is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.ScanningProblem, is FeedbackEmailType.CardAttestationFailed, -> addPhoneInfoBody() - is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.cardInfo) - is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.cardInfo) - is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.cardInfo, type.visaTxDetails) + is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo) + is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) + is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails) } return build() } - private suspend fun FeedbackDataBuilder.addVisaRequestBody(cardInfo: CardInfo, visaTxDetails: VisaTxDetails) { - addUserRequestBody(cardInfo) + private suspend fun FeedbackDataBuilder.addVisaRequestBody( + walletMetaInfo: WalletMetaInfo, + visaTxDetails: VisaTxDetails, + ) { + addUserRequestBody(walletMetaInfo) addDelimiter() addVisaTxInfo(visaTxDetails) } - private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { - addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId)) + private suspend fun FeedbackDataBuilder.addUserRequestBody(walletMetaInfo: WalletMetaInfo) { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(walletMetaInfo.userWalletId)) addDelimiter() - addCardInfo(cardInfo) + addUserWalletMetaInfo(walletMetaInfo) addDelimiter() - val userWalletId = cardInfo.userWalletId + val userWalletId = walletMetaInfo.userWalletId if (userWalletId != null) { val blockchainInfoList = feedbackRepository.getBlockchainInfoList(userWalletId) @@ -68,11 +71,11 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } - private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) { - addCardInfo(cardInfo) + private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(walletMetaInfo: WalletMetaInfo) { + addUserWalletMetaInfo(walletMetaInfo) addDelimiter() - val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( @@ -91,10 +94,10 @@ internal class EmailMessageBodyResolver( } private suspend fun FeedbackDataBuilder.addStakingProblemBody(type: FeedbackEmailType.StakingProblem) { - addCardInfo(type.cardInfo) + addUserWalletMetaInfo(type.walletMetaInfo) addDelimiter() - val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" } + val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( @@ -120,10 +123,10 @@ internal class EmailMessageBodyResolver( } private suspend fun FeedbackDataBuilder.addSwapProblemBody(type: FeedbackEmailType.SwapProblem) { - addCardInfo(type.cardInfo) + addUserWalletMetaInfo(type.walletMetaInfo) addDelimiter() - val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" } + val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( @@ -144,8 +147,8 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } - private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { - addCardInfo(cardInfo) + private fun FeedbackDataBuilder.addCardAndPhoneInfo(walletMetaInfo: WalletMetaInfo) { + addUserWalletMetaInfo(walletMetaInfo) addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 849d1ce0c4..490c6611dd 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -18,7 +18,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { fun resolve(type: FeedbackEmailType): String { return when (type) { is FeedbackEmailType.DirectUserRequest -> { - if (type.cardInfo.isStart2Coin) { + if (type.walletMetaInfo.isStart2Coin == true) { resources.getStringSafe(R.string.feedback_subject_support) } else { resources.getStringSafe(R.string.feedback_subject_support_tangem) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index e02206355e..cf7047bb68 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -11,16 +11,15 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.card.common.TapWorkarounds.isVisa -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.component.DetailsComponent @@ -56,7 +55,7 @@ internal class DetailsModel @Inject constructor( paramsContainer: ParamsContainer, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val appStateHolder: ReduxStateHolder, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val feedbackFeatureToggles: FeedbackFeatureToggles, @@ -121,20 +120,15 @@ internal class DetailsModel @Inject constructor( val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null") - if (selectedUserWallet is UserWallet.Hot) { - return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Send feedback - } - - val scanResponse = selectedUserWallet.requireColdWallet().scanResponse - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch + val metaInfo = getWalletMetaInfoUseCase(selectedUserWallet.walletId).getOrNull() ?: return@launch val feedbackType = when { userWallets.all { it is UserWallet.Cold && it.scanResponse.card.isVisa } -> - FeedbackEmailType.Visa.DirectUserRequest(cardInfo) + FeedbackEmailType.Visa.DirectUserRequest(metaInfo) userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } -> - FeedbackEmailType.DirectUserRequest(cardInfo) + FeedbackEmailType.DirectUserRequest(metaInfo) else -> { - showFeedbackEmailTypeOptionBS(cardInfo) + showFeedbackEmailTypeOptionBS(metaInfo) return@launch } } @@ -144,18 +138,14 @@ internal class DetailsModel @Inject constructor( } private fun openUseDesk() { - val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null") - - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] UseDesk + modelScope.launch { + val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null") + val metaInfo = getWalletMetaInfoUseCase.invoke(userWallet.walletId).getOrNull() ?: return@launch + router.push(AppRoute.Usedesk(metaInfo)) } - - val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return - - router.push(AppRoute.Usedesk(cardInfo)) } - private fun showFeedbackEmailTypeOptionBS(selectedCardInfo: CardInfo) { + private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo) { state.update { it.copy( selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig( @@ -171,7 +161,7 @@ internal class DetailsModel @Inject constructor( content = SelectEmailFeedbackTypeBS( onOptionClick = { option -> onEmailFeedbackTypeOptionSelected( - selectedCardInfo = selectedCardInfo, + selectedWalletMetaInfo = selectedWalletMetaInfo, option = option, ) @@ -189,32 +179,33 @@ internal class DetailsModel @Inject constructor( } private fun onEmailFeedbackTypeOptionSelected( - selectedCardInfo: CardInfo, + selectedWalletMetaInfo: WalletMetaInfo, option: SelectEmailFeedbackTypeBS.Option, ) { modelScope.launch { val feedbackType = when (option) { SelectEmailFeedbackTypeBS.Option.General -> { - if (selectedCardInfo.isVisa.not()) { - FeedbackEmailType.DirectUserRequest(selectedCardInfo) + if (selectedWalletMetaInfo.isVisa == false) { + FeedbackEmailType.DirectUserRequest(selectedWalletMetaInfo) } else { - val scanResponse = getWalletsUseCase.invokeSync() - .firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa.not() } - ?.requireColdWallet()?.scanResponse ?: return@launch + val userWallet = getWalletsUseCase.invokeSync() + .firstOrNull { + it is UserWallet.Hot || it is UserWallet.Cold && it.scanResponse.card.isVisa.not() + } ?: return@launch - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch - FeedbackEmailType.DirectUserRequest(cardInfo) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + FeedbackEmailType.DirectUserRequest(metaInfo) } } SelectEmailFeedbackTypeBS.Option.Visa -> { - if (selectedCardInfo.isVisa) { - FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo) + if (selectedWalletMetaInfo.isVisa == true) { + FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo) } else { - val scanResponse = getWalletsUseCase.invokeSync() + val userWallet = getWalletsUseCase.invokeSync() .firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa } - ?.requireColdWallet()?.scanResponse ?: return@launch - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch - FeedbackEmailType.Visa.DirectUserRequest(cardInfo) + ?: return@launch + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + FeedbackEmailType.Visa.DirectUserRequest(metaInfo) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index a864a30018..1567ffe625 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse @@ -39,7 +39,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val cardRepository: CardRepository, private val analyticsHandler: AnalyticsEventHandler, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, @@ -168,7 +168,8 @@ internal class MultiWalletCreateWalletModel @Inject constructor( fun navigateToSupportScreen() { modelScope.launch { - val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + val cardInfo = + getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 0c7b8d6290..1e9e8c838e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.CardDTO @@ -48,7 +48,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val backupServiceHolder: BackupServiceHolder, private val tangemSdkManager: TangemSdkManager, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, @@ -329,7 +329,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( private fun navigateToSupportScreen() { modelScope.launch { - val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + val cardInfo = + getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index c59be4fb41..2bdce1a527 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -10,7 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.crypto.bip39.Mnemonic import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.features.hotwallet.MnemonicRepository @@ -44,7 +44,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private val urlOpener: UrlOpener, private val tangemSdkManager: TangemSdkManager, private val cardRepository: CardRepository, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsHandler: AnalyticsEventHandler, ) : Model() { @@ -257,7 +257,8 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( fun navigateToSupportScreen() { modelScope.launch { - val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + val cardInfo = + getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 6a15db448c..af0fadb5fb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -10,7 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.domain.card.common.TapWorkarounds.isVisa -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent @@ -24,7 +24,7 @@ import kotlinx.coroutines.launch internal class DefaultOnboardingStepperComponent @AssistedInject constructor( @Assisted val context: AppComponentContext, @Assisted val params: OnboardingStepperComponent.Params, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsHandler: AnalyticsEventHandler, ) : OnboardingStepperComponent, AppComponentContext by context { @@ -40,7 +40,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( ) componentScope.launch { - val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch + val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa) { FeedbackEmailType.Visa.Activation(cardInfo) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 5fcc8c9505..27f86bfa82 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -20,14 +20,12 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase @@ -90,7 +88,7 @@ internal class SendConfirmModel @Inject constructor( private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -340,15 +338,9 @@ internal class SendConfirmModel @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + val metaInfo = getWalletMetaInfoUseCase.invoke(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index f0a63844c1..e47d5db337 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo @@ -26,7 +26,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase @@ -91,7 +90,7 @@ internal class SendModel @Inject constructor( private val parseQrCodeUseCase: ParseQrCodeUseCase, private val sendConfirmAlertFactory: SendConfirmAlertFactory, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, @@ -472,15 +471,9 @@ internal class SendModel @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 0cd5ed9676..8b9888ff75 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -16,13 +16,11 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase @@ -78,7 +76,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger, private val notificationsUpdateListener: SendNotificationsUpdateListener, @@ -249,15 +247,9 @@ internal class NFTSendConfirmModel @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 11d9d7ba78..6f1080a389 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -14,7 +14,7 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo @@ -22,7 +22,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.tokens.* import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase @@ -71,7 +70,7 @@ internal class NFTSendModel @Inject constructor( private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val alertFactory: SendConfirmAlertFactory, private val sendFeatureToggles: SendFeatureToggles, @@ -224,15 +223,9 @@ internal class NFTSendModel @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo)) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 31e0e9e58c..3fa6edebdd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo @@ -34,7 +34,6 @@ import com.tangem.domain.models.staking.* import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.staking.* import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent @@ -107,7 +106,7 @@ internal class StakingModel @Inject constructor( private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, private val vibratorHapticManager: VibratorHapticManager, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, @@ -859,13 +858,8 @@ internal class StakingModel @Inject constructor( modelScope.launch { val network = cryptoCurrencyStatus.currency.network - if (userWallet is UserWallet.Hot) { - return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) - .getOrElse { error("CardInfo must be not null") } + val metaInfo = + getWalletMetaInfoUseCase(userWallet.walletId).getOrElse { error("CardInfo must be not null") } val amountState = uiState.value.amountState as? AmountState.Data val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data @@ -887,7 +881,7 @@ internal class StakingModel @Inject constructor( ) val email = FeedbackEmailType.StakingProblem( - cardInfo = cardInfo, + walletMetaInfo = metaInfo, validatorName = validator?.name, transactionTypes = transactionsInProgress.map { it.type.name }, unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 9ea883b4e5..d5360e2add 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -8,14 +8,13 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.swap.v2.impl.R import javax.inject.Inject @@ -24,7 +23,7 @@ import javax.inject.Inject internal class SwapAlertFactory @Inject constructor( private val uiMessageSender: UiMessageSender, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, ) { fun getGenericErrorState(expressError: ExpressError, onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -96,16 +95,11 @@ internal class SwapAlertFactory @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return sendFeedbackEmailUseCase( type = FeedbackEmailType.SwapProblem( - cardInfo = cardInfo, + walletMetaInfo = metaInfo, providerName = confirmData?.quote?.provider?.name.orEmpty(), txId = txId.orEmpty(), ), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 2e8026f8db..bc6725502d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo @@ -33,7 +33,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -94,7 +93,7 @@ internal class SwapModel @Inject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, @@ -1364,15 +1363,11 @@ internal class SwapModel @Inject constructor( ), ) - if (userWallet is UserWallet.Hot) { - return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId) .getOrElse { error("CardInfo must be not null") } val email = FeedbackEmailType.SwapProblem( - cardInfo = cardInfo, + walletMetaInfo = metaInfo, providerName = dataState.selectedProvider?.name.orEmpty(), txId = transaction?.txId.orEmpty(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 4a492e3a06..0c9b0a698c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -4,7 +4,7 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.visa.GetVisaCurrencyUseCase @@ -39,7 +39,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( private val eventSender: WalletEventSender, private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val getUserWalletsUseCase: GetWalletsUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -101,13 +101,13 @@ internal class VisaWalletIntentsImplementor @Inject constructor( val userWalletId = stateController.getSelectedWalletId() val userWallet = getUserWalletsUseCase.invokeSync() .firstOrNull { it.walletId == userWalletId } ?: return@launch - val cardInfo = getCardInfoUseCase.invoke( + val cardInfo = getWalletMetaInfoUseCase.invoke( userWallet.requireColdWallet().scanResponse, ).getOrNull() ?: return@launch sendFeedbackEmailUseCase( FeedbackEmailType.Visa.Dispute( - cardInfo = cardInfo, + walletMetaInfo = cardInfo, visaTxDetails = txDetails, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 5e3757b744..7fae09097d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -11,7 +11,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase -import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency @@ -107,7 +107,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val urlOpener: UrlOpener, @@ -244,15 +244,9 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( neverToSuggestRateAppUseCase() val userWallet = getSelectedUserWallet() ?: return@launch + val cardInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch - if (userWallet is UserWallet.Hot) { - return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val scanResponse = userWallet.requireColdWallet().scanResponse - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch - - sendFeedbackEmailUseCase(type = FeedbackEmailType.RateCanBeBetter(cardInfo = cardInfo)) + sendFeedbackEmailUseCase(type = FeedbackEmailType.RateCanBeBetter(walletMetaInfo = cardInfo)) } } @@ -292,15 +286,9 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSupportClick() { val userWallet = getSelectedUserWallet() ?: return - if (userWallet is UserWallet.Hot) { - return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow - } - - val scanResponse = userWallet.requireColdWallet().scanResponse - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(cardInfo = cardInfo)) + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(walletMetaInfo = metaInfo)) } } From 2934b42af5fd5548760d0a209b6c00598ba45743 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 12:17:12 +0300 Subject: [PATCH 35/48] Updated on 2026-08-14 --- .gitignore | 8 ++ .../tap/di/domain/OnrampDomainModule.kt | 6 ++ .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + core/res/src/main/res/values-es/strings.xml | 5 +- core/res/src/main/res/values-ja/strings.xml | 8 +- core/res/src/main/res/values-ru/strings.xml | 21 ++++- .../src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 2 +- .../res/drawable/img_notification_sepa.webp | Bin 0 -> 14202 bytes .../tangem/utils/coroutines/CoroutineExt.kt | 23 +++++ .../data/onramp/DefaultOnrampRepository.kt | 45 ++++++++++ .../data/promo/DefaultPromoRepository.kt | 17 ++-- .../domain/onramp/model/OnrampSource.kt | 1 + .../onramp/OnrampSepaAvailableUseCase.kt | 80 ++++++++++++++++++ .../onramp/repositories/OnrampRepository.kt | 6 ++ .../tangem/domain/promo/models/PromoBanner.kt | 1 + .../analytics/TokenSwapPromoAnalyticsEvent.kt | 13 +-- .../onramp/component/OnrampComponent.kt | 7 +- .../onramp/main/OnrampMainComponent.kt | 1 + .../main/entity/factory/OnrampStateFactory.kt | 4 + .../main/model/OnrampMainComponentModel.kt | 25 +++++- .../onramp/root/DefaultOnrampComponent.kt | 1 + ...okenDetailsNotificationsAnalyticsSender.kt | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 4 +- .../intents/WalletWarningsClickIntents.kt | 45 +++++++--- .../utils/WalletWarningsAnalyticsSender.kt | 8 +- .../domain/GetMultiWalletWarningsFactory.kt | 56 ++++++++++-- .../wallet/state/model/WalletNotification.kt | 17 ++++ 29 files changed, 360 insertions(+), 49 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_notification_sepa.webp create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSepaAvailableUseCase.kt diff --git a/.gitignore b/.gitignore index 0144ddc6e7..a4d9c4c73d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,14 @@ # Built application files /build /buildSrc +**/build/.transforms/** +**/build/classes/** +**/build/generated/** +**/build/intermediates/** +**/build/kotlin/** +**/build/libs/** +**/build/outputs/** +**/build/tmp/** # Local configuration file (sdk path, etc) local.properties diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 335ce4196c..f5c4fda538 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -120,6 +120,12 @@ internal object OnrampDomainModule { return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver) } + @Provides + @Singleton + fun provideOnrampSepaAvailableUseCase(onrampRepository: OnrampRepository): OnrampSepaAvailableUseCase { + return OnrampSepaAvailableUseCase(onrampRepository) + } + @Provides @Singleton fun provideOnrampUpdateTransactionStatusUseCase( diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 9a80508e7b..708e4bc745 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -207,6 +207,7 @@ internal class ChildFactory @Inject constructor( userWalletId = route.userWalletId, cryptoCurrency = route.currency, source = route.source, + launchSepa = route.launchSepa, ), componentFactory = onrampComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 20d91d8960..d2204b5cd1 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -238,6 +238,7 @@ sealed class AppRoute(val path: String) : Route { val source: OnrampSource, val userWalletId: UserWalletId, val currency: CryptoCurrency, + val launchSepa: Boolean = false, ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c70f5846b2..170d0a6610 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -878,7 +878,7 @@ Memo Compruebe su conexión de red Información de tarifa de red no accesible - Tú envías + Envía Desde De %s Límite de gas @@ -951,10 +951,13 @@ Intercambiar y enviar ¿Continuar con la conversión? Esto borrará sus datos anteriores. Confirmar Conversión + El envío de cualquier otra moneda supondrá su pérdida irreversible. + Seleccione la red de destino correcta Envía cualquier token y lo convertiremos en el camino. Su destinatario obtiene exactamente lo que necesita, sin problemas. El destinatario recibirá Al destinatario Cantidad a recibir + El destinatario recibe %s ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. Enviar con swap Transacción enviada diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 76ddd053e5..ff93a67733 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1043,13 +1043,13 @@ 受信トークンを変更してもよろしいですか? 変更すると、以前入力したデータがリセットされます。 トークンの変更 スワップして送信 - 変換を続行しますか? これにより以前のデータは消去されます。 + スワップを実行しますか?これにより以前のデータは消去されます。 変換を確定 その他の通貨を送信すると、取り返しのつかない損失が発生します。 正しい受信者ネットワークを選択してください - 受け取るトークンを自由に選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。 - 受信者は受け取ります - 受取人へ + 受け取るトークンを選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。 + 受信者が受け取ります + 受信者へ 受取金額 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9fda11100a..f21a1bdd98 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,5 +1,11 @@ + Архив + Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно + Аккаунт + Новый аккаунт + Продолжить редактирование + Несохраненные изменения Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки. Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи. Продать @@ -309,6 +315,7 @@ Legacy адрес Получить активы Отправка средств в другой сети может повлечь потерю средств. + %s сети Отправляйте средства, используя только Привет, команда поддержки, у меня возникла ошибка с кодом: %s Ошибка WalletConnect @@ -410,7 +417,16 @@ Сканировать в %s В сети %s + Вы уверены, что хотите прервать процесс создания кода доступа? + Аппаратный кошелёк Будьте в курсе новых функций и новостей + Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек. + Во время операции произошла ошибка. + Приватные ключи будут перемещены из приложения в вашу Tangem карту или кольцо + Миграция ключей + Начать апгрейд + Tangem кошелек + Сделать апгрейд кошелька до аппаратной версии. Эта информация была сгенерирована ИИ.\nНажмите здесь, если обнаружили ошибку. Чтобы изменить код доступа, приложите карту или кольцо как показано выше и не убирайте до окончания операции Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции @@ -928,7 +944,7 @@ Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные. Изменение токена Обмен и отправка - Продолжить с конвертацией? Это действие удалит предыдущие данные + Продолжить с обменом? Это действие удалит предыдущие данные Подтвердить конвертацию Выберите любой токен к получению. Ваш адресат получит ровно то, что вы выбрали — без лишних сложностей. Будет получено @@ -1100,6 +1116,7 @@ Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку. Вы отправляете Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. + Высокое влияние на цену Недостаточно средств Дать разрешение Обменять @@ -1228,9 +1245,11 @@ Получить с 10% скидкой Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. Откройте Tangem Wallet + Изменить Код Доступа Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem. В настоящий момент push-уведомления могут не работать на устройствах Huawei. Мы уже работаем над решением этой проблемы и планируем исправление в ближайших обновлениях. Спасибо за понимание! Уведомления о транзакциях + Установить Код Доступа Настройки кошелька Tangem Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку 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 14ac369e2e..5fa3e94450 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1367,6 +1367,7 @@ Будь ласка, згенеруйте новий URI та спробуйте ще раз Термін для з’єднання минув Прогнозовані зміни + Пропоновано %s Поповніть баланс, щоб покрити комісію мережі Недостатньо %1$s Додайте %s мережі до вашого портфелю для цього гаманця diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1a6894683e..f24d713213 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1060,7 +1060,7 @@ Are you sure you want to change the receiving token? This will reset your previously entered data. Changing token Swap and send - Proceed with conversion? This will clear your previous data. + Proceed with swap? This will clear your previous data. Confirm Conversion Sending any other currency will result in its irreversible loss. Select the correct recipient network diff --git a/core/ui/src/main/res/drawable/img_notification_sepa.webp b/core/ui/src/main/res/drawable/img_notification_sepa.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ca547ac85f2750c4f514dc03b4342568167571f GIT binary patch literal 14202 zcmV-=H-*SjNk&F;Hvj-vMM6+kP&iCwHvj-HzrZgLO+akhNRkBEbx*^d`~&WuCxnRp zPXJ(;55Xq-5Ia}DxYo*AUuY?7`2ajoY+Ew>lOG>^>fHcanyc-LJw4xf8;(f=N&wSn zu**nc#ukHsBnBc0$sA^Y2&e>>D-R)&wtB+`&j@fc1Cg?Y%*+6%CV?af4xfIdZ_@Tz zw?N+&pd263^6=U7?-p=l+g9By$@Iftfcvk5Ayi~$W{dUa%^TAvK?86k*;d`OWDJ?< z)BacDSmnqi#t+vlE=i6gIg(?m8*^$j68ry$ElCHDT9YFi{ht7U#TRq&>t21u6vb*N z#2R8ve~bD&4PE0nq;WCc$nwV&ETQY1%F_7#m{fn7LKZClWbH3$_%THR^B)D>U-|J- z7j&Pi_+cRzPr9VQkOV~cU8VV*9Ig5xWyUSg&aJ-CUk)&&F7-s!&zz{%^BwdS~MMTbW7Zq{v*G7B9>}-cn z?1)@NlIT*BNKyf(o$c^rI^$t0dB2K?oJCzxDVi=H%E1&_Ia(f-Mun4-YWjs-Rb<2I5MRplS{(89qtA|?QED+`yoE8;@H zrGPng0L)pKb*n$=Qp_1zGBx5-Zl>-3v*Nb-&#pnT!_0QTkTNqfzd`T%AlwJ&ReR6O z%wc9`#wr|1R-~Q4?wL6weYSV~98v`WF!pz{9IVrQ^T4s=OWtvD;hfx`u zX{0jD-}BsbXlpyRIs3lfcP6&&q)KJmcG|IH+qP}nc5geWtZF>p``nbaZBttz5AN{c z?9N~-Yl=I`3a8Bqr^$*X?Zpph=NFs=TUmmx;7o95iaT@G*oQlOxN|{}B)M%fCl?6? zc##)S*$?n^YuoZR*0$}(ah?}Qm%x%Sj)RpHbKT6$%*@Pp>u%n4ckdQ6{s3mCVrB>y zgbk7{y6`-YW30McRm)4Q8!maeZQHbMZQDw1t@knJoNKRjwvKMwwr&1{JF55{WcZHr zSJ(_%kL~ApbFDQo#^|kjvTa+nUD>uut*!Sl=UgCZ+hJyAW`<~-XdL&6#0dhrPAra_ znUAqH$Rw?$ImhU&QlPEvm|N|CGD$s|+P2%+D3kiywr$(CZQt6qZQE^7C*z21Bt>!- zj+yZfb_oA{@$*n^|KY2STDQ)*_TGgmAy8LyF;B*8?-_4eetvzL3n-b#UVwOlz7vlN zZsHMFalKnc&Jrjhi3y=J3EW~GKIiMm1J!eYZS&T<5bcy7?s|Sn?xwr4cL%O4Eg!8F z(sM*a6?Uo1D}})h(sws?JA5?q4B(@=>QM;q7PoC%4-ZB+y-M69b`5rPjU4zvq(3<`>HE|3%gFo$2!5ovtQEdzm_(jAwEuW;xI8bLi!4d@R=fDJ z?Y@rU6jH?ygsG9twsk#~A%BJ4cpB($ZG%yX8Fdw4*8^LtdNa4L) z--eeyvf(rI1M52^3FC$Vtc!E0kbMhrNYei6!O4xMtvO>ESD8e0nU5~WD}q6LCYIXn zwC#?ytWLPLYfn7Cn7y3V_SVTr?^|&jWI7yArsh zMF>fA(BDIqXmn=_&8VYM(!YDK`TCLRcx{g9vRUmH=yq^6{-~_IiVfOQN4rZbF^DY$ zSSfiw?0iM&SbnkFjOp!lh)!tS>4me@qjS%fLj#*CBsDh$Tbien&QjjnEw|Y8<_mo= z?D-=4s2qJA>)thbP)3z)5{Kzns)9FcT5~SzfqP|dj)j-#Zc=I*#Lzsiz-YYi>3QF0 z&dDK+tcJ6BN+%R~!Y=8)UARKzskxgD@`7HTti8hKI3(90eOF}6BF44MMwDBV5?>;Q z1G1N;8#yu%4w3U>Yr2Z)#Txg7_wUxnfY4mS%U(m*6`n;*)lxSXarMz|JEX@{##G*& zCC`@L-OchY!nfPtYPmVbfn$6J_j)Hw!`f&KEhvzbYL-hYS~rm(x`Z3ku8OP(*EZq4 zq?(^^J7-W|vb%_>u!1TWFKJFL#Oc@x3a?%ZS9Jw#cO-AeIs z$lXDghRea1z@Z^NO+vioR$#9oIbQiO93aIoS%TML$GUosJzU#e`{)%M>^jmRnad>O z<|5dNk5Zne@KtLG$M&U3+kSw9>q9w-UrKR1hE}ivn=|}?E^?q#(DVB~dBr7YPWv&n zzZF`Ca4mX~Vp*^*{>~csNY{qbh*?#eA(_@tXCtROKmqoVFrO|PR3Npw7Lc=uV zW8*@?M8zXScotVuYspMD=F~l>_~pl!eGPQaE@mq10H*!ng+_K(xK|A5vLtDX(L|NBN~d29M= zX}x)t-ia1ygi0WR44_s3pa29AmcS+!QLsaSFY`7HNu8Rx1R$37Z8xi#2#NsFT}qXZ zHE4y7$dN(fiNCh@{yOk6%FkMwH}_8dhyN0BB{@v*i`_@#&6cNO5nKSE7zjlGKmd@y z76N4sqKFw#;Vqs4Dc+-gF0mc_pXMx20BAmKnG-jKjDUqKR5nv_k7V@ko>d=|tOh;q zymL6ArM z$=CkT_Mzq`mZfe@Vnrdls902iGTMaCG9H8bl1PukXq>Q+o@MX3X$?6Kj()t8|8rH{ z!Cpyw#vPK!8saf20z?UbngC=>uq>MfpG(U{WL`ss@f5OpZw>)CJJa2SinEJ3xQKqB zF@EI6iU2G? z5CO@8EhZvGM7yRFCtOIJWWao&-z-ODhmhq|R zd_u3UT9iJAt&bjr+aco-6P6t;CW93mqLRIfP!5WGFoyty4MWfYxy5#g*TLr?n(|!Y zjIe5B{_qWaetzcDYTc&ChF!{GV^^j-(+G^nPmxo4i^<8#Q}|7)L-2aib%X|FMLz0$ z>azNEHx9V}d6s^1LK35v?qz%uk_iR?S`7&aT;MRBcd1&E6R}7Po->(t(HH{Oru^aa zc}!4dgBQl182C?2n9l#g6t5@tIPoy^AW9CyW792kjeJ7g~eL6wwfs)EZF1wWw^pJ~$3bI*}M8HnJIRc8wvk zIM2x)ev)r#S#s|F&cL*${Ti0e%ceP!-&()Z8IeyM8R`lXf?%3q1Gt+7twy~zk|m>l6alwl&Mim8pQ!Exr8rssI- zw;tv19e@Ck1UeCltS9nCk6ca~17s?b5S3_%1|*PWoSMb0n^wEv3XMr?Dq4$gAorOK zsK^_>Zuh;d-uO=Lzs1hT0EW>b4|XvgkYR3wTOz81Av*im`m9g%C-JR#CZQDdfeZlv zL9%1>MF@a2KZvu>clYj^Jc7oc2m}q$00dDRZNqJQo95M03@T`BXe-s>5H6)XvSjYe zH(4(X+fx1t!e5Re!%q>rgh$ZU)J;ytg{9NQ{h#^9$#42w`M3Oy;_cU5H$@1kIKWWM zPQ%Hod+T=xNZ8F@4vo`t+Xc%KpG7eZsGuQ>qM_kZt($y)(u$2}P@hRj@O0O>1GV|r zyzwRb?`*!OPwqRn?t))Nq8LS6Yt3E+1|zhJR13yre(F2y|C>KE`ruDp`}w!>tG9d; zCPHq=2GDI6C)!1GEbIorArzu9B|Hf8Oj96PKmkFC3_CN+xIh7=X>4el^4KMhVPC8* zfF<+BZScr+K1|L?0IA8;!Xb7MHcafy5~@R76CF3%Pv4pS+{r|*`3woLoAph@)-UT> z7_fr{09KF$pur>2J(^B5iUw4oLM6mw0}pw7C$0-bflqi7|8N(%m?eMCoAD%6P4@NR zJEz?eWi7Cst=)nzLKO_`DoWTyvTjHQg@A1Lc_vIHmV;$EIX0F9H|uf@=o!+(`SW?>)cCqSk~F)uOa^eq zI1x%rAsCIL1uMW|Il?E3Sn`55Ofezdi;x!)t%rif21iytEq9NB}V7 z>+!r(LpC5nA#RS{tfWR9ss0YOsDtL*PC28p} z4oE-~ZfD7BaTmd-&!kzpXx=#al4x36pR1QC2SaUc6^z6VF&6F-Zc;{Y6lckEDG2Wn zi`4}g445{K_QD@DQ~=a95Q#x$qQQ?B{QQ>6R02YUBG3$TmLi^6Hc)62(+2sU!f%PW zx_b}&Rq*kBIl8_ds&BmbP-pPyeD3ruk@MFaZNRBRrOikAsR#~;B4p;xsF#pHAq0UM z6&x)vibN)qz$1z<6Cq42d=01o00B_1_bFzmbmDZ+AFiZ|0!0+_KrD@Dvk~z)AqAQk zS@X{FXW~Ee+~F&hue$-@H71fbNeu4|PY>BG@NrA?7%^zeyeCpHKwC{hYGkd-;zggPM!g@wz2{|CJee{+8H zp0`N&czE=!MEJ6Lu8*%IH|jabD>**5R_&@FTE0$x`X=&nu&2ouNTLG{D~HWC+jZ$( zs!6iwN>l#+d)plS3oD6-1RyLMIpa7uiFe`kj3==YQ$`()VO2CmLLrlt5-|i*3C07J z)j$dsfI3du3hUG`=Id0uP5AEDa_?fDa9b$ER*B9-`TT&#ZhP#L zZ@&Dazoh>5j;=@B>muLWzie-lTzgy3@Fehio%Ms(wt+t~=GzR{Z;&2`;&L&)4Cy+R zLolQ|91LcoX<8N2^L=wAN0Hy_(nI3T|8v*Vp$26vYH>`hBa*&jTR0H(W{o0a5z}(F z${_}5AVL)a0D_esObp99IRd;lgZNhZ^eq0&FB!eMaf>Jb0h$`sDoHN;15uSmizvf} zH5X$;-UvUPZUuxtTJPLnao?5YOIiKKC1qa&%eXbpWfF&eyw%rhiQkCEd!k{tzw$oc zWdHcm;C7haB7b!!Tn2lza+p|3hV%g@2fP+#M8s}Z#;$7)3E$kY3V1^?8M1bJ)67Lw zY@rn(Ay*hn3<`+@A}HW&s0-sD1j3Pc(5XhAa@{NUkNRKn-nGjc+wZ}zP@qDsA>Kk6 zel4V%<=iBJsG;G~SPFjinR%47FXy8U03V_E7xw-7;k+vsJgocY`1f-cuBpO`u}*8| zGnqb>^{=?jFsw(}cJIB*qv74Kv&GHAbq&)(VdIh|qOWLK*f#c54$FsJ0!L`4mK~qZ zDc^OxwmTe9)yu_BO4Op;uo@N=q`^m! z{$z5vbM4V*c3HecB@jSB0X5Y!;ys$B5c*?xf}7R6iXTYAklY$yt)Q@OGzXaR&1`Ys z_3j$pKW`U|?IBrik#6DGGOaI&3`y)vc)Q2M?V|478gfOrq}Z+IM_5NwNJ?6pB^!E@ zhAajF5jGJp&~!~#LAO|F-kCoU-@5vi%MJ%4bu8ls00wGUOF-au;Ao0y1VYMasZdP^ zpN$A@gYx~#S&D<@S2DY~Q(oWcT}5#?+dclU%r{DasG(RuO`{dDQ^6;8b~rN2nYcN+ zzv&;!+6(FA(%h-&r3Vf*LLvC2H$L?Kw5={qBiH&-4UL9-R#_i|E%>nQh-bUJp!Tj~ z5X1GHCWyNAd)u62Wr}u>!qeb#GQ2I*d*sW6O(3&@CatdsYvDbr2Cv{_^RJ_*sMNy_ z$BuBNy=Wmpf$`9qpixW)WmAO&d>~87iK@2%@$)Mc-_5mempzEdBMDaIp9%ir|KmUP z-?;gK_@{(G6HqK5AcCT4G)Zu7GC#|9W_Pl@m#u4@?$hzblHK0y`2ENPnD7Y1e`Dv` zQ|fC9C5s_A>*@3;)+Ix3s6CZ~DUjdlS` zW8-NoY&6{84Ow2lJo&n&3^N&D$B8DN(s#E#Lg%gECl@qr{N9Qy)&2C3Ki>%8L;F{a+`DSO ze>XB*z`diKE?C=5+Up+vsmjxBb3LS_t+2bVNh7eGwmpT+!}PMb@%n+5^v5$F(@>uy z8lv$GhA~Lbn%a2{j^Yt-6E}DfA&3upaTRf5U5bXRTiL)Z;D&0RG3`*i+A6_ug8|gg zVx}c%jeAH4Iz%WXh2RlPHy7i}_6Izo5@L|{T6f$eXS|W0cV6$L37>!h0zyb)W8hwz zyR+4r?!328xf~we?T)|oUiZlm?0_4hn6?6d59?{ZF1;JPXq=(POHK^wflTE*@mSe& zK6@*MeOc(MJH4z#*)iU9nQ4M2;>>xcBW)D0Zq1}*J) z)|3&2Fha@;|vlI1&eEldT0*_XrU5ERDxHGvklhp zKm$-)Uu7e&M*W@mEQLZ95QwM=u*7OGA+`+w@kN%8a^@Hhj=1;mWv8#druFzHyizlO z59#myon1=y$7~*XD@Ew+2GZdm@Vet~*u3U@``Q#g#@Ic9iSKleAxvVnJ$%fTE7V zdO2|paZ0xd5vE$WE$xbp}?>s@2k(A=#Ed!56}!avmE zUw!tD@9gKm-rZe?Q^ZZ%%M6*XIL?;e`8qCR=i_Czgd!$lnfJm5N>NbMFj+5(nhZ<8 z4YYU+G*Yk+o@jY#(@G|9qXG~z2r(7{PEzfS+S1tH8kN@pLQ6n>U)f*@;sU_O;nvws zXPc!pfa}4MG9eNTA@G5798y695KurMfdU+8XlUxhXC!^oq=Bf3Foj@9GGyB0f9c83 z@~R7V^c%bhF9%)BdC{}%q5l}mpRV!DW%`e8`Nvf*j*Gp9#5bQ@ZQXb^9tsZpB{V}~dviU}eX5ENo53Ir1h5NH5}5aykj*Hi@xaZ4N>nZq0+ z6>5P&fD9mPFv6hH&@SJLGqwV>3a_5tuCwh});XKeJqmg6ysS1mUYO44SgJG$(SQm> z)KGu`0Pz6|w6tk+G0GOTb|zY<8W8}%fDnbSI@bBr_xuU);nMgd<3)d#dXcwAvIci{ zkQZXv)!B~DZ0#NzvRuPc5$6eI)2WM&kt)2xV+U#(0uTk(cu5bz8$=6jw!2;A`G^S8 z2bRx-aYHs=kyyITDx|V$0S-mfLJk5z*a!_JrqM>oy9;qEpk)A5G!C~PlI^B*dPFLD z$IIV$_G$C3vzhN`NPLI}5MT#6h=7_ZY5*Xla*P)ah=aIV?&XXRp$|X^A&e*hH7GW- z=NG@|k7EE2z8T_xdJOz7I#HWhZYf@k4-O>z-gqB2EqzbV)6N5}sa5xml@+nHi(9oK)aBG;lLb-CDF1jgqM5i>47JXbo)X*TO02XAdHxZ6QY0y4kDGLnkxV? zfDhqmIF)R~#h5sZrJ&JPaM{E@ED4p{6{NH@)=m3lW@)}1Qb9w;^E93=6hWBr5_<6} zB4JBBV7!B`s(j)F>u3whX;Hw?CT=6bS=fWM0a1hu1`*aCK*h9%H9sufH1`9h0UWXb z3sygcht~7GBgb9q)i)S(@lVt8`#K!V%V!*RU$Xo$;DWeEOkfAX(Ytr2cPl>JVNSa?HEJM?+yt>>XkKunT7Ns} zIFyudSo4f{v@C49-y7~P3{Aoi#%fDTaN*#vkajU>ty7sF6uR+V0;8#u_W zeHct7^JZEyT?=nvQkM4k)K9zm(zpJ@*8F*0zY&dtsT1}$AifDO@F@ryJhyk_Yz266 z_Qf}~8Du|J`9s)9#-tVy8%XvaK0!y^r(Vas)9?D^{cm~b>X(=Y ze{DBywv@?skIN{Gt7!p-2^hmVJXRE@hz|Q?d&il$y>REv>lJ{Ojeq}|ccE|D^DLg7 z9;WxGVqY&J35K9grkAi6yPNC|riZzLULV54VP)Q&*0ePZEB#YVzRK69C28Hgh4J(6 zdCO&7zru2Fte=prJwz;iShZO>FCjMB!6^bJD~a0_XFJdn?FGAG_UI;@hGaJ!-^C|0 zd>Oy#bH`uX9Pk{r6O)iklg0qdR{^0aWFZ4aT^9BOfpK?v>4Wg}ul#7%037O0eYTQh z-2asxPpxog(NUrc;<84;3N)F!8qkN{Qcj$(Ia~C2GB-QyKRvGf?PrxgeI0NZz?+SF zKWcrp(M49)m&|%q^suLvr%XGR+(7u8VwE5#lA)DBmZ%NQ>*3VR@W@o|kGS7)WHm;n zmN8O9VTwgaR<@z^q#=7h@kpp8$>l9QMBcQ%?=}ZhNl0KV7S@ zUDaG*MhP}aHt8&~vdTssS~J|WZSOGNv!a^!ulMere*zr(C=Tn9(|^yO9$#!V|8F+* zm&2484;&~;6&G!38GxFwt{I6{v(PL7;Nj>zZ`QxekQ9HUk>7=+23DF4cQqGcfO!|=a z&GhYWk?FrVc3b-;j!>WyfSM3sL?H_(OfgK%L?KxsZ<1$F+WMtR)-KzhciiS{z#f24 zoB*K*KWmGaWs6MMPqg3D@fW#O04hJi!-@SI%~wa|Y(AMZ+XAhra6)>K(R_)=q;^uF z)vDNtkf{`c3sD3B7#Khy4@4mZDl9Wzkk7)nX?v8#`7oYeea9~stL6aAwEzog*US9g z+OMy2i6DxQ1!1NZ$jGMiAeb0VQ{{_XCk|5vFtH0^L;(Uo7`c(aJeEf5xBs(t{??C% z%MUCBn0tNMXYi2jKB)RMjUofEkc9zMEZ~5#>10|TWSkT!8V9r`@PSkasR4i}nDz5= zVVhLd_m0j_Kd~NQ9%kT6o%S8Rm$VJuLIqO-gozr2NW7p`WE&(0pJ&!#bTE#JRZ(Q^ zn5Cvx6B92yI?n-^mjRA?cYb-;Cv=`4{R@m-xQq}8N@2j7I3@09&D|S@qkIOX6Ep81 z_vu5PSH3l|8epyl*ajH*+Gp<3JO6L;d(flHYRjdV_E}53PUUde?bpfZv$eS#Zw|`c zKW}^P`K!GE^Ef~WU?f0Y14;q@M@WO4uXk-Y#sOG!0G9g^l7WSh;io`S31tiMF{=R(&h_AM<;SNNjH*i)w$4q0M=x5q1u`Fig+PXF)+H7RsDk;Mr)+|$ z3Kj)WFM2m$FWp*|$O);85v3xC7-@Y&MxfsG&c{plt^cZ9mb%GcDx1{E^eO3rw1_es zUId3aPL#NPj2n(EZ53Qnfk3(79S|`x>Om(0H3d!uX&$V6J|D4URrwVx5=|BH;|3G+ zn_qcwIp_F$`Q1zC0#VBb$Fp@B7Z;7|d(hdgzATI2rf>6%3^--=fUWPJsC7Z4MD-71MK`q-ve5UQ+#>rKj@`}Fv&hd;c? zb*=voFR{Pn5&Qb@$318)ACVe^lx1-9RL3F%1j{7otQsdW>QywBMx#;fH|g>}l8{mg zKr!g5z^&u;f4a>+Z!9f=h*^L}hFU*MRxEkA4wki9u{5J-T;qBs;uKMCE3w=x&bD>Z zuI#PZ0YILJnk@rcmjQ1lij{{asQU}D$8;# z`{tcI;2Op)1A@>INhHGDDq>0vbz{cT9aB3SD!~!Yy8(kRgn*t&GeDNJ*0EF1Hi^nw zpo&nhG}RR-INU``jo*IMXqs}1b zh71}h!$MtyH2@fkJ%)#c4^Uk%_YUH-$Sb8pHq zoynTRm<(tjqLAxV=|G7jct>DzMN2AJ6u}eV9{zShbLNlQhg#6sQ8j#}Iv^z8Wki~StxXXJmb>l}CY>@U0gvpzT;WX`p zASC4%8q!;h=n7+CfRM4iN*3x$+g zLcrJ_V5GGUAU9YD$e^4A)K7rAF(n|2@LPv{-B2ebXpPDo$+5#>RU{eOH35eMS}RCP zL0a@4E zNvIA-I}H=&?;SzMq6R&Y{2`gH_0nYKp6SutQ*bG{tp)*TDLvSP4^8!e=5WE%iKWvo zD4WicO`0+@^-2c0y5u{_+> zKRYelpOi`7(O`N_Hym(M7bqZr&|fj@5dyGak3ICv-F3A&B+LMvlu|c~SO6Rl3!qBx zXpkLK%Svt?W2%RRGvTjX5JOX7@`iL?2R6V-HCnMm>EsMDg3QSUlGq~(B!M;@AnH-w z5YEOiPMJRQ&B_Y|5}lpGSSy{?)2*h7ywgc63lM$xWmA%~O=y~ba;_fAHe^|rM;Nl2 zDu|zMHZ;et1H#osU&QSE&q)LE-lTydvrvM=e`#08EQfLgZ!b-%Ko4kvfJjUaRCh4y zb^fOYniB!Sh?@rHlCXN3=yzs6vI-Wp7wnT#fTSljWWX5!p#Z`_Vz!x--d=dS5B2$9 zg1Ld5h+NX9df^~`Gu_!%tVmYEKi<+$mPduYzg_>R3S(+e$F^UbxlXv94D^ z5U~;r2dNCG#}>Vymw7M5thfnT3AL0^Wu~OfD1Y(Ksaw4lKNRHv(?>$-JxtZ!F~&qi zr1h*zvB6R_bP{O-O;~p>GxaKR)sS8PMuqp4-mtS4J^!wELlh@9W zo!9rn_{|Zutx6V!OD|z|T17w8|6=6h4#FW}T<~X()de-hgH8fe&;4}b^5GvYa(yqp zj&%m0GuS<-y(7zH2N~9)j@o_BsiBnh`7_7rfou`>jjeje@1%-+%sia?(3e-v@B8u^ zdG^QSoqdL#`ci762(N(ECL>z!$ZBl|iQp@1XLe?PZDFN({d8umsYAMtceF--^;D+X z=*5line{Z%2Txo(QvglaP)TX10dxwO0nD`Cu|Q*%RQ0Q9iG^l{=YdU#2|)q05VYPA zSVC)pdRaRsY-i9qfRcns2TmF~b&)V32^!ahM6!W2uT=Bzq&wI`OMwUFJ1eo}zr(F+ zaZ_NGD-hjT7%&zIDp#NXRS^@Q7W(P-fFCei*mN=t(AAXCmttX)j7rH<_+(I%bjv3+bws&42~6W-ZI4sfG(q6vd0DNQz@0kmX*!UoXT{3}zi z0y~8Nb%^1&bq7t|>=@MO=#Du3-&*?rBdxyy{FKi;3ZdE6*5LU4-Y2P~72Um7GTGh= zPfx?u|Ic-op069ut?mIZhyC=YeG>Vahp+mrbaKYhg+TSDscP36sx$nZ`I5|Kc*1D5 z;Gp7p>v#XdQ)2hg*L6PHZuIsOW~52Wnn&{0acV~ z#cpzd#-!_rGi*uo(C0lQJUblupN#TPC;gYBGF)MRZ1?cjPjGcLd@=aj$-e|&V9Ja^ z7O|;JElws0s6pWZ*}O5Jb6T2r)9RVa=iaUJL!WwNtlz6+YpM&kP)-sgPA%Ih=^Hnz zpZ;aXr>_`XzRu{q7p{Epjmpc$Y+*|x%OU`_%vcN{Ll7_c2Fftew8;ufe>*InOY2-& z4L%p0XJ&9fvaa0bP-ZL*>q2Lnk_*%Qq4myJ-T&l2EMI?ypG_B@e~&Ale4BEaWQecg zU8sc1!Xa@<(`bT-uo9Ai@W40NT!Ms2TM{BkJS5mE+FtgYc?A$Uqi^ZuO&8XluExU+4+f?<}BpD$j(2Ulj%{{x_Csj24?45OZ z)hByL^Q)O5c8DFO1_4M&AO#yLQJ@G21Yn?`Auz}wgfN3x5DaNcLJiz;vT)!@e4IZc zZPTX?W;^<5dJvD~01}Y|NJLlYUbXx7@@dE7YkAef#Z9g)lMdk_WHTk+;RPk2U<(t9 z7(zk_KtY5^q#y_oGXM!;j6_7kFcej*6i#Troqo3HIes*@1>_6s52+z9=*{>--bf); zCP9b*H}G{^dDuR)>|`$Kv0e7^Y{p~}46p}N-~u6B!W0!iXn+W&ali@+*rEc|BtuH1 zOc5TUGV6#GjfgNJQ`spQgtiiQRi4f3-u2%OfIMLZnowWGf(HW^5+srpFc!vQ=H=A% zc^i-09j9eyU0!v&GE7II5?H7Jgb`!m0%2@n7XTn+KtLE1tbhdq0XWp56fOW!fgKk3 zj%mjG(xI&Q-JrL3`!$p&uYDebUz}+hZpU>loXL<46Gs#VR;`(b*8HqLeJZRcQgO-1 z7bAo%KnPiog#ZvjjD<0_7#mXzFlrpZgrSjy00rQH0R$E@G(_Es9<%+boQh+U%D=&x zAO3y5t(_;U%wxBMX$`2He8^X-sY?(hBM1bEM2l5p7HPXFo16_5AR%GF5|S{&7LpO5 z0wiQ%6^q$2gDu!%1dgBx2m}CNEK~rfeP7C0X7gH8I!pXEzIeL*(zfdB+j5J3_SVTqA2MhF2&$bdnJ!2*h**fPRM z(0~X&APQAbL4g|1lE0RoVaPy-=Q0}vpD0UIBHLqI7MrX=gNQr0oSZ~7-s@_pzn>UCI` zT|2GJqBL|isDVJxThG?ptUbbwQaWZxA#nk%lCUv?_4`rSGQ8CWgFTv zHm2xPupu3iN>ZT!6>tiXC{2MNN+?i45FbJa01Ff_0t-MOEC?(}QUlbr%E+OJ0#T8X zSXJ7q#W;WV_$h1Z99iX&1My3{+3WaN#d}+Z=O$?~V6*f)wlaLQqOd=Z3Lbc)6bd9K zst6wd0iXbe0+GO>f+XNTfC3~K83Y0_77GG|XaGPZ0JS1+C{`8At3?x>J!-Pu<^#^m zk5%s3lW@;|rbcL3Pr6v#Ewe#jDeXWHRBA&G`@+mk5pfitkQ7EH)MbW5C?HUi1wer! z8X&N!p+Eot5*UgAKp+4~y--XLUwyI~OauMZ?8L?ytOUg7$0`GR>@)tU)5mdXbIjo@ zrWw=PwjP14c*UUFC>N*jpc2YLi4+wxWF|~Ps%Sog2nG{B3&fEjQIBL*sI@>z05WC;$)FTLQxXLO_V{!LA5!2rK~>V<^@HJrG zbmuyUbPy0AJ7_dp}gT}&_ap@2^?XB5F|*mU@f-b zLj-t*6BaInKx}jiv1BA-BkRy87=NMXYYmGY1q#TexMlt5rHGMkbfOMg#oi9l1{2T- zP1pfzNTTfKCWr!RqyrNiu%JP-jE*HSjNy9co_=^1hK3Ozl{R`LwFweTvn5@5KsJV5BTc8DKXj?5=6K_)r8$5N=MQe(giC*n@}V}d?HCipfnW|*mwht+b^xU(6>o|?Ev$z`<0m{ z=;JhqLIRu-j4%K)?*^d-HjEx1VJJ~SBq~^98anL88aEtXb^sHG6YKu_5y@QiSz{|Q zY-17dK2&4L)AEE6kMvyjlLZ~AMw7L zuN}L^g{6Ux1;cj11!Q7QS7K)Do_n?XpE-AY{pI@F8IZT}DOdh25*JgI=C*5%7MFPA z3NGEI))vM8r;?c$8mj^3a-V$Qrg+M$wutGPqd8+{yd4XwHOo$(^b literal 0 HcmV?d00001 diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 901cf071d6..b57c8e0015 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -1,6 +1,8 @@ package com.tangem.utils.coroutines import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -54,4 +56,25 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } } } +} + +@Suppress("LongParameterList", "MagicNumber") +inline fun combine6( + flow1: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + @Suppress("UNCHECKED_CAST") + transform( + arr[0] as T1, + arr[1] as T2, + arr[2] as T3, + arr[3] as T4, + arr[4] as T5, + arr[5] as T6, + ) } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index a4216c1fd9..1bbb60d6ea 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -261,6 +261,48 @@ internal class DefaultOnrampRepository( storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await()) } + override suspend fun hasMercuryoSepaMethod( + userWallet: UserWallet, + currency: OnrampCurrency, + country: OnrampCountry, + cryptoCurrency: CryptoCurrency, + ): Boolean { + return withContext(dispatchers.io) { + val onrampPairs = + safeApiCall( + call = { + onrampApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = OnrampPairsRequest( + fromCurrencyCode = currency.code, + countryCode = country.code, + to = listOf( + OnrampDestinationDTO( + contractAddress = cryptoCurrency.getContractAddress(), + network = cryptoCurrency.network.backendId, + ), + ), + ), + ).bind() + }, + onError = { + Timber.w(it, "Unable to fetch onramp pairs") + throw it + }, + ) + + val mercuryoProvider = onrampPairs.map { it.providers }.flatten() + .find { it.providerId == MERCURYO_PROVIDER_ID } + val hasSepaMethod = mercuryoProvider?.paymentMethods?.any { it == SEPA_METHOD_ID } ?: false + + hasSepaMethod + } + } + override suspend fun fetchQuotes(userWallet: UserWallet, cryptoCurrency: CryptoCurrency, amount: Amount) = withContext(dispatchers.io) { val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) { @@ -554,5 +596,8 @@ internal class DefaultOnrampRepository( const val PROVIDER_THEME_LIGHT = "light" const val REDIRECT_URL = "https://tangem.com/onramp" + + const val SEPA_METHOD_ID = "sepa" + const val MERCURYO_PROVIDER_ID = "mercuryo" } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index efdaae18b9..f58b3d0613 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -38,24 +38,19 @@ internal class DefaultPromoRepository( key = PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), default = true, ).map { shouldShow -> - if (promoId == PromoId.Referral) { - runCatching { + when (promoId) { + PromoId.Referral -> runCatching { !referralRepository.isReferralParticipant(userWalletId) && shouldShow }.getOrDefault(false) - } else { - shouldShow + PromoId.Sepa -> shouldShow } } } override fun isReadyToShowTokenPromo(promoId: PromoId): Flow { - return if (promoId == PromoId.Referral) { - flowOf(false) - } else { - appPreferencesStore.get( - PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), - default = false, - ) + return when (promoId) { + PromoId.Referral -> flowOf(false) + PromoId.Sepa -> flowOf(false) } } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt index 86b72907ad..e193dbc3b2 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampSource.kt @@ -6,4 +6,5 @@ enum class OnrampSource(val analyticsName: String) { TOKEN_LONG_TAP("Long Tap"), TOKEN_DETAILS("Token"), MARKETS("Markets"), + SEPA_BANNER("SEPA Banner"), } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSepaAvailableUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSepaAvailableUseCase.kt new file mode 100644 index 0000000000..fb43156695 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSepaAvailableUseCase.kt @@ -0,0 +1,80 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.model.OnrampCurrency + +class OnrampSepaAvailableUseCase( + private val repository: OnrampRepository, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + currency: OnrampCurrency, + country: OnrampCountry, + cryptoCurrency: CryptoCurrency, + ): Boolean { + if (country.code !in SEPA_AVAILABLE_COUNTRY_CODES) { + return false + } + + return Either.catch { + repository.hasMercuryoSepaMethod( + userWallet = userWallet, + currency = currency, + country = country, + cryptoCurrency = cryptoCurrency, + ) + }.getOrElse { false } + } + + companion object { + val SEPA_AVAILABLE_COUNTRY_CODES = listOf( + "AL", // Albania + "AD", // Andorra + "AT", // Austria + "BE", // Belgium + "BG", // Bulgaria + "HR", // Croatia + "CY", // Cyprus + "CZ", // Czech Republic + "DK", // Denmark + "EE", // Estonia + "FI", // Finland + "FR", // France + "DE", // Germany + "GR", // Greece + "HU", // Hungary + "IS", // Iceland + "IE", // Ireland + "IT", // Italy + "LV", // Latvia + "LI", // Liechtenstein + "LT", // Lithuania + "LU", // Luxembourg + "MT", // Malta + "MD", // Moldova + "MC", // Monaco + "ME", // Montenegro + "NL", // Netherlands + "MK", // North Macedonia + "NO", // Norway + "PL", // Poland + "PT", // Portugal + "RO", // Romania + "SM", // San Marino + "RS", // Serbia + "SK", // Slovakia + "SI", // Slovenia + "ES", // Spain + "SE", // Sweden + "CH", // Switzerland + "GB", // United Kingdom + "VA", // Vatican City + ) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index b4879d8cc7..0a7ad02041 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -15,6 +15,12 @@ interface OnrampRepository { suspend fun getCountriesSync(): List? suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus + suspend fun hasMercuryoSepaMethod( + userWallet: UserWallet, + currency: OnrampCurrency, + country: OnrampCountry, + cryptoCurrency: CryptoCurrency, + ): Boolean suspend fun fetchCurrencies(userWallet: UserWallet) suspend fun fetchCountries(userWallet: UserWallet): List suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet) diff --git a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt index 81f592f146..7605016350 100644 --- a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt +++ b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt @@ -27,4 +27,5 @@ data class PromoBanner( enum class PromoId { Referral, + Sepa, } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt index 7ed6c36cc2..f460d229ff 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -9,24 +9,24 @@ sealed class TokenSwapPromoAnalyticsEvent( ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { class NoticePromotionBanner( source: AnalyticsParam.ScreensSources, - programName: ProgramName, + program: Program, ) : TokenSwapPromoAnalyticsEvent( event = "Notice - Promotion Banner", params = mapOf( AnalyticsParam.SOURCE to source.value, - "Program Name" to programName.name, + "Program Name" to program.programName, ), ) class PromotionBannerClicked( source: AnalyticsParam.ScreensSources, - programName: ProgramName, + program: Program, action: BannerAction, ) : TokenSwapPromoAnalyticsEvent( event = "Promo Banner Clicked", params = mapOf( AnalyticsParam.SOURCE to source.value, - "Program Name" to programName.name, + "Program Name" to program.programName, "Action" to action.action, ), ) { @@ -37,7 +37,8 @@ sealed class TokenSwapPromoAnalyticsEvent( } // Use it on new promo action - enum class ProgramName { - Empty, + enum class Program(val programName: String) { + Empty("Empty"), + Sepa("Sepa"), } } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt index 7bdb6d8901..3de1a19fb7 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -8,7 +8,12 @@ import com.tangem.domain.models.wallet.UserWalletId interface OnrampComponent : ComposableContentComponent { - data class Params(val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, val source: OnrampSource) + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val source: OnrampSource, + val launchSepa: Boolean = false, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index 57c298a510..fa5cf89457 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -15,6 +15,7 @@ internal interface OnrampMainComponent : ComposableContentComponent { val source: OnrampSource, val openSettings: () -> Unit, val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, + val launchSepa: Boolean, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 8eb611e8fe..bfd3db959d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -148,4 +148,8 @@ internal class OnrampStateFactory( decimals = currency.precision, type = AmountType.FiatType(currency.code), ) + + companion object { + const val PREDEFINED_SEPA_AMOUNT = "100" + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 50acb8a5a6..f78daf8e79 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -28,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory import com.tangem.features.onramp.providers.entity.SelectProviderResult import com.tangem.features.onramp.utils.sendOnrampErrorEvent @@ -64,6 +65,8 @@ internal class OnrampMainComponentModel @Inject constructor( private val params: OnrampMainComponent.Params = paramsContainer.require() + private var isSepaLaunched = false + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val stateFactory = OnrampStateFactory( @@ -148,11 +151,16 @@ internal class OnrampMainComponentModel @Inject constructor( if (country == null) return@onEach _state.update { if (it is OnrampMainComponentUM.InitialLoading) { - stateFactory.getReadyState(country.defaultCurrency) + val state = stateFactory.getReadyState(country.defaultCurrency) + if (params.launchSepa) { + onAmountValueChanged(PREDEFINED_SEPA_AMOUNT) + } + state } else { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) } } + updatePairsAndQuotes() }, ) @@ -317,8 +325,18 @@ internal class OnrampMainComponentModel @Inject constructor( private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } + val sepaQuote = if (params.launchSepa && !isSepaLaunched) { + isSepaLaunched = true + + quotes.filterIsInstance().firstOrNull { + it.provider.id == MERCURYO_PROVIDER_ID && it.paymentMethod.id == SEPA_METHOD_ID + } + } else { + null + } + // Check if amount, country or currency has changed - val newQuote = if (checkLastInputState(quoteToCheck)) { + val newQuote = sepaQuote ?: if (checkLastInputState(quoteToCheck)) { quoteToCheck } else { val state = state.value as? OnrampMainComponentUM.Content @@ -420,5 +438,8 @@ internal class OnrampMainComponentModel @Inject constructor( private companion object { const val UPDATE_DELAY = 10_000L + + const val MERCURYO_PROVIDER_ID = "mercuryo" + const val SEPA_METHOD_ID = "sepa" } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index e08b4a4477..e9ee2b3295 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -81,6 +81,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( ), ) }, + launchSepa = params.launchSepa, ), ) is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index dbe31e24b9..c02b4801b9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -38,7 +38,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( currency = cryptoCurrency, ) is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action + program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action source = AnalyticsParam.ScreensSources.Token, ) is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index d0e4d0f29c..795dbe542e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -850,7 +850,7 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send( TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action + program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, ), ) @@ -863,7 +863,7 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send( TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action + program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 7fae09097d..28d03a317a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -19,6 +19,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher @@ -75,7 +76,7 @@ internal interface WalletWarningsClickIntents { fun onClosePromoClick(promoId: PromoId) - fun onPromoClick(promoId: PromoId) + fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency? = null) fun onSupportClick() @@ -92,7 +93,7 @@ internal interface WalletWarningsClickIntents { fun onFinishWalletActivationClick() } -@Suppress("LongParameterList") +@Suppress("LargeClass", "LongParameterList") @ModelScoped internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, @@ -260,26 +261,46 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onClosePromoClick(promoId: PromoId) { analyticsEventHandler.send( - if (promoId == PromoId.Referral) { - MainScreen.ReferralPromoButtonDismiss - } else { - TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( + when (promoId) { + PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss + PromoId.Sepa -> TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action + program = TokenSwapPromoAnalyticsEvent.Program.Sepa, action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, ) }, + ) modelScope.launch(dispatchers.main) { shouldShowPromoWalletUseCase.neverToShow(promoId) } } - override fun onPromoClick(promoId: PromoId) { - if (promoId == PromoId.Referral) { - val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate) - appRouter.push(AppRoute.ReferralProgram(userWalletId = userWallet.walletId)) + override fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency?) { + val userWallet = getSelectedUserWallet() ?: return + when (promoId) { + PromoId.Referral -> { + analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate) + appRouter.push(AppRoute.ReferralProgram(userWalletId = userWallet.walletId)) + } + PromoId.Sepa -> { + analyticsEventHandler.send( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + program = TokenSwapPromoAnalyticsEvent.Program.Sepa, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, + ), + ) + cryptoCurrency ?: return + appRouter.push( + AppRoute.Onramp( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + source = OnrampSource.SEPA_BANNER, + launchSepa = true, + ), + ) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index d75237b768..bd539e7397 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent -import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.ProgramName +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.Program import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -51,7 +51,11 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.NoteMigration -> MainScreen.NotePromo is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, - programName = ProgramName.Empty, // Use it on new promo action + program = Program.Empty, // Use it on new promo action + ) + is WalletNotification.Sepa -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.Sepa, ) is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index a7e173397b..5bd5ebfce1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -1,5 +1,8 @@ +@file:Suppress("MaximumLineLength") + package com.tangem.feature.wallet.presentation.wallet.domain +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint @@ -14,9 +17,12 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.onramp.GetOnrampCountryUseCase +import com.tangem.domain.onramp.OnrampSepaAvailableUseCase import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase @@ -24,11 +30,13 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin +import com.tangem.utils.coroutines.combine6 import com.tangem.utils.extensions.isPositive import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -41,19 +49,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val backupValidator: BackupValidator, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase, + private val getOnrampCountryUseCase: GetOnrampCountryUseCase, ) { - @Suppress("MagicNumber", "MaximumLineLength") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - return combine( - flow = tokenListStore.getOrThrow(userWallet.walletId), + return combine6( + flow1 = tokenListStore.getOrThrow(userWallet.walletId), flow2 = isReadyToShowRateAppUseCase(), flow3 = isNeedToBackupUseCase(userWallet.walletId), flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), flow5 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral), - ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo -> + flow6 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo, shouldShowSepaBanner -> buildList { addUsedOutdatedDataNotification(maybeTokenList) @@ -63,6 +74,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) + addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner) + addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents) @@ -217,6 +230,39 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } + private suspend fun MutableList.addSepaPromoNotification( + userWallet: UserWallet, + clickIntents: WalletClickIntents, + shouldShowSepaPromo: Boolean, + ) { + val currencies = getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId).getOrElse { + Timber.e("Error on getting crypto currency list") + return + } + + val bitcoinCurrency = currencies.find { isBitcoin(it.network.rawId) } ?: return + + val country = getOnrampCountryUseCase.invokeSync(userWallet).getOrElse { + Timber.e("Error on getting onramp country") + return + } + + val isSepaAvailable = onrampSepaAvailableUseCase( + userWallet = userWallet, + country = country, + currency = country.defaultCurrency, + cryptoCurrency = bitcoinCurrency, + ) + + addIf( + element = WalletNotification.Sepa( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.Sepa) }, + onClick = { clickIntents.onPromoClick(promoId = PromoId.Sepa, bitcoinCurrency) }, + ), + condition = shouldShowSepaPromo && isSepaAvailable, + ) + } + private fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver?, tokenList: Lce, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index dd6ae1ec2b..f1459ac5d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -287,4 +287,21 @@ sealed class WalletNotification(val config: NotificationConfig) { iconSize = 54.dp, ), ) + + data class Sepa( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.notification_sepa_title), + subtitle = resourceReference(R.string.notification_sepa_text), + iconResId = R.drawable.img_notification_sepa, + onCloseClick = onCloseClick, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.notification_sepa_button), + onClick = onClick, + ), + iconSize = 54.dp, + ), + ) } \ No newline at end of file From f466395589df2b7364316453e4bbdf40be4d5499 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 20:22:30 +0400 Subject: [PATCH 36/48] Updated on 2026-08-14 --- .../main/assets/configs/feature_toggles_config.json | 4 ++++ data/account/build.gradle.kts | 11 +++++++++++ .../com/tangem/data/account/di/AccountDataModule.kt | 9 +++++++++ .../featuretoggle/DefaultAccountsFeatureToggles.kt | 12 ++++++++++++ .../account/featuretoggle/AccountsFeatureToggles.kt | 11 +++++++++++ gradle/tangem_dependencies.toml | 2 +- 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 9d99dceed5..3cf7a9e285 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -70,5 +70,9 @@ { "name": "NEW_ONRAMP_MAIN_ENABLED", "version": "undefined" + }, + { + "name": "ACCOUNTS_FEATURE_ENABLED", + "version": "undefined" } ] diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 90800c1228..ace90489e6 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // region Project - Core implementation(projects.core.datasource) + implementation(projects.core.configToggles) api(projects.core.utils) // endregion @@ -29,6 +30,16 @@ dependencies { implementation(projects.data.common) // endregion + // region Project - Libs + implementation(projects.libs.crypto) + implementation(projects.libs.blockchainSdk) + // endregion + + // region Tangem dependencies + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) + // endregion + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index df872e31dc..61056db1c3 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -1,11 +1,14 @@ package com.tangem.data.account.di +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.account.converter.AccountConverterFactoryContainer +import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles import com.tangem.data.account.repository.DefaultAccountsCRUDRepository import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.store.ArchivedAccountsStoreFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -18,6 +21,12 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object AccountDataModule { + @Provides + @Singleton + fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles { + return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager) + } + @Provides @Singleton fun provideAccountsCRUDRepository( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt b/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt new file mode 100644 index 0000000000..0a6accf03f --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.data.account.featuretoggle + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles + +internal class DefaultAccountsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : AccountsFeatureToggles { + + override val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED") +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt b/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt new file mode 100644 index 0000000000..289b2a02e6 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.account.featuretoggle + +/** + * Accounts feature toggle + * +[REDACTED_AUTHOR] + */ +interface AccountsFeatureToggles { + + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 33e667f9d0..a9c2010b63 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1212" +tangemBlockchainSdk = "develop-1213" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-560" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8cff4c1bbae3bad1cef82ca8bbec2132de708ca6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 15:27:17 +0300 Subject: [PATCH 37/48] Updated on 2026-08-14 --- .../DefaultUserWalletsListRepository.kt | 19 ++++++--- .../wallet/child/wallet/model/WalletModel.kt | 14 +++++++ .../model/WalletsUpdateActionResolver.kt | 39 +++++++++++++++++++ .../intents/WalletContentClickIntents.kt | 8 ++++ .../intents/WalletWarningsClickIntents.kt | 22 +++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 93810c48da..87edec2202 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -62,7 +62,7 @@ internal class DefaultUserWalletsListRepository( // If we don't save persistent information, we don't need to load user wallets // and we should clear any existing data clearPersistentData() - userWallets.value = emptyList() + updateWallets { emptyList() } return } @@ -118,7 +118,7 @@ internal class DefaultUserWalletsListRepository( } // update the userWallets state and add if it doesn't exist - userWallets.update { currentWallets -> + updateWallets { currentWallets -> val wallets = currentWallets ?: emptyList() if (wallets.any { it.walletId == userWallet.walletId }) { wallets.map { if (it.walletId == userWallet.walletId) userWallet else it } @@ -248,7 +248,7 @@ internal class DefaultUserWalletsListRepository( removePasswordAttempts(userWallet) sensitiveInformationRepository.getAll(listOf(encryptionKey)) - .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) } @@ -306,7 +306,7 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(allKeys) .doOnSuccess { sensitiveInfo -> - userWallets.update { it?.updateWith(sensitiveInfo) } + updateWallets { it?.updateWith(sensitiveInfo) } } .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } @@ -318,7 +318,7 @@ internal class DefaultUserWalletsListRepository( raise(LockWalletsError.NothingToLock) } - userWallets.update { + updateWallets { it?.map { if (it.walletId !in unsecuredWalletIds) { it.lock() @@ -389,6 +389,15 @@ internal class DefaultUserWalletsListRepository( return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication } + private fun updateWallets(block: (List?) -> List?) { + userWallets.update(block) + + selectedUserWallet.update { currentSelected -> + if (currentSelected == null) return@update null + userWallets.value?.find { it.walletId == currentSelected.walletId } + } + } + /** * Find the nearest available wallet that can be selected * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index e7a00027c6..d90595d32f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -363,6 +363,9 @@ internal class WalletModel @Inject constructor( is WalletsUpdateActionResolver.Action.RenameWallets -> { stateHolder.update(transformer = RenameWalletsTransformer(renamedWallets = action.renamedWallets)) } + is WalletsUpdateActionResolver.Action.ReloadWarningsForWallets -> { + reloadWarnings(action) + } WalletsUpdateActionResolver.Action.EmptyWallets -> { Timber.w("Wallets list is empty!") } @@ -372,6 +375,17 @@ internal class WalletModel @Inject constructor( } } + private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWarningsForWallets) { + action.wallets.forEach { + walletScreenContentLoader.load( + userWallet = it, + clickIntents = clickIntents, + coroutineScope = modelScope, + isRefresh = true, + ) + } + } + private suspend fun initializeWallets(action: WalletsUpdateActionResolver.Action.InitializeWallets) { stateHolder.update( transformer = InitializeWalletsTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 3e9a4d114b..5c27b29b42 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import timber.log.Timber @@ -75,10 +76,20 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletNameChanged(state, wallets) -> { getRenameWalletsAction(state, wallets) } + isAnyHotWalletBackedUp(state, wallets) -> { + getHotWalletsBackedUpAction(state, wallets) + } else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) } } + private fun isAnyHotWalletBackedUp(state: WalletScreenState, wallets: List): Boolean { + val incompleteActivationWalletIds = state.incompleteActivationWalletIds() + return incompleteActivationWalletIds.mapNotNull { walletId -> + wallets.firstOrNull { it.walletId == walletId && it is UserWallet.Hot && it.backedUp } + }.isNotEmpty() + } + private fun isWalletsCountChanged(state: WalletScreenState, wallets: List): Boolean { val prevWalletsSize = state.wallets.size val walletsSize = wallets.size @@ -145,6 +156,20 @@ internal class WalletsUpdateActionResolver @Inject constructor( return prevWalletsIds == newWalletsIds && isAnyNameChanged } + private fun getHotWalletsBackedUpAction( + state: WalletScreenState, + wallets: List, + ): Action.ReloadWarningsForWallets { + val incompleteActivationWalletIds = state.incompleteActivationWalletIds() + + val walletsToUpdate = wallets.filter { + it is UserWallet.Hot && it.backedUp && + incompleteActivationWalletIds.contains(it.walletId) + } + + return Action.ReloadWarningsForWallets(walletsToUpdate) + } + private fun getRenameWalletsAction(state: WalletScreenState, wallets: List): Action.RenameWallets { val prevWallets = state.wallets.map { it.walletCardState.id to it.walletCardState.title } val newWallets = wallets.map { it.walletId to it.name } @@ -199,6 +224,16 @@ internal class WalletsUpdateActionResolver @Inject constructor( ?: error("Previous selected wallet is not found") } + private fun WalletScreenState.incompleteActivationWalletIds(): List { + return wallets.mapNotNull { + if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { + it.walletCardState.id + } else { + null + } + } + } + private fun List.indexOfWallet(id: UserWalletId): Int { val selectedIndex = indexOfFirst { it.walletId == id } @@ -323,6 +358,10 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } + data class ReloadWarningsForWallets( + val wallets: List, + ) : Action() + data object EmptyWallets : Action() data object Unknown : Action() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index cc5a440140..da6c773a77 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -24,6 +24,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBot import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take @@ -76,9 +77,16 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val walletEventSender: WalletEventSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + router.openDetailsScreen(stateHolder.getSelectedWalletId()) + return + } + + // Will be removed after Hot Wallet release modelScope.launch(dispatchers.main) { val userWalletId = stateHolder.getSelectedWalletId() val userWallet = getUserWalletUseCase(userWalletId).getOrElse { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 28d03a317a..203128c7ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetWalletMetaInfoUseCase +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency @@ -44,6 +45,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomShe import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -117,6 +119,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -168,6 +172,22 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onOpenUnlockWalletsBottomSheetClick() { analyticsEventHandler.send(MainScreen.WalletUnlockTapped) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onLeft { + val selectedUserWallet = getSelectedUserWallet() ?: return@onLeft + val method = when (selectedUserWallet) { + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan + is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode + } + userWalletsListRepository.unlock(stateHolder.getSelectedWalletId(), method) + } + } + return + } + + // Will be removed after hot wallet release stateHolder.showBottomSheet( WalletBottomSheetConfig.UnlockWallets( onUnlockClick = this::onUnlockWalletClick, @@ -176,6 +196,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) } + @Deprecated("Will be removed with hot wallet release") override fun onUnlockWalletClick() { analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics) @@ -203,6 +224,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( walletEventSender.send(event) } + @Deprecated("Will be removed with hot wallet release") override fun onScanToUnlockWalletClick() { analyticsEventHandler.send(MainScreen.UnlockWithCardScan) openScanCardDialog() From ab6072e12d92d2ccecd04eb0e598425647ddff01 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 17:49:20 +0400 Subject: [PATCH 38/48] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 39 +++++++++++++++ .../tangem/tap/di/data/VisaStorageModule.kt | 6 +++ .../tasks/visa/VisaCardActivationTask.kt | 6 +-- .../tap/domain/visa/VisaCardScanHandler.kt | 12 ++--- .../tangem/datasource/api/pay/TangemPayApi.kt | 3 ++ .../pay/models/response/CustomerMeResponse.kt | 48 +++++++++++++++++++ .../datasource/local/visa/TangemPayStorage.kt | 10 ++++ .../DefaultTangemPayAuthDataSource.kt | 45 +++++++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 2 +- .../{ => repository}/DefaultKycRepository.kt | 48 ++++++++----------- .../visa/DefaultVisaActivationRepository.kt | 6 +-- ....kt => DefaultVisaAuthRemoteDataSource.kt} | 7 ++- .../com/tangem/data/visa/di/VisaDataModule.kt | 12 +++-- .../pay/datasource/TangemPayAuthDataSource.kt | 8 ++++ .../domain/pay/repository/KycRepository.kt | 8 ++++ .../VisaAuthRemoteDataSource.kt} | 4 +- .../model/OnboardingVisaAccessCodeModel.kt | 6 +-- .../model/OnboardingVisaInProgressModel.kt | 6 +-- 18 files changed, 219 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt rename data/visa/src/main/kotlin/com/tangem/data/pay/{ => repository}/DefaultKycRepository.kt (60%) rename data/visa/src/main/kotlin/com/tangem/data/visa/{DefaultVisaAuthRepository.kt => DefaultVisaAuthRemoteDataSource.kt} (97%) create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt rename domain/visa/src/main/kotlin/com/tangem/domain/visa/{repository/VisaAuthRepository.kt => datasource/VisaAuthRemoteDataSource.kt} (93%) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt new file mode 100644 index 0000000000..1b9e1d54d7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.data + +import android.content.Context +import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.sdk.storage.AndroidSecureStorageV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +private const val DEFAULT_KEY = "tangem_pay_default_key" + +@Singleton +internal class DefaultTangemPayStorage @Inject constructor( + @ApplicationContext applicationContext: Context, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : TangemPayStorage { + + private val secureStorage by lazy { + AndroidSecureStorageV2( + appContext = applicationContext, + useStrongBox = false, + name = "tangem_pay_storage", + ) + } + + override suspend fun store(authHeader: String) = withContext(dispatcherProvider.io) { + secureStorage.store(authHeader.encodeToByteArray(throwOnInvalidSequence = true), DEFAULT_KEY) + } + + override suspend fun get(): String? = withContext(dispatcherProvider.io) { + secureStorage.get(DEFAULT_KEY)?.decodeToString(throwOnInvalidSequence = true) + } + + override suspend fun clear() = withContext(dispatcherProvider.io) { + secureStorage.delete(DEFAULT_KEY) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt index c78909a676..1927963c23 100644 --- a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.data +import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.tap.data.DefaultTangemPayStorage import com.tangem.tap.data.DefaultVisaAuthTokenStorage import com.tangem.tap.data.DefaultVisaOTPStorage import dagger.Binds @@ -21,4 +23,8 @@ internal interface VisaStorageModule { @Binds @Singleton fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage + + @Binds + @Singleton + fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 74ed0d0827..9dd041b2ea 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -22,7 +22,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.pins.SetUserCodeCommand @@ -46,7 +46,7 @@ class VisaCardActivationTask @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, private val otpStorage: VisaOTPStorage, private val visaAuthTokenStorage: VisaAuthTokenStorage, - private val visaAuthRepository: VisaAuthRepository, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : CardSessionRunnable { @@ -168,7 +168,7 @@ class VisaCardActivationTask @AssistedInject constructor( signedChallenge: VisaAuthSignedChallenge, cardWalletAddress: String, ): Either = either { - val tokens = visaAuthRepository.getAccessTokens(signedChallenge) + val tokens = visaAuthRemoteDataSource.getAccessTokens(signedChallenge) .getOrElse { raise(it.tangemError) } visaAuthTokenStorage.store(cardId, tokens) diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index c439ee3d5b..d074006981 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -15,7 +15,7 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaCardScanError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse import com.tangem.operations.attestation.AttestWalletKeyResponse @@ -26,7 +26,7 @@ import javax.inject.Inject import kotlin.coroutines.resume internal class VisaCardScanHandler @Inject constructor( - private val visaAuthRepository: VisaAuthRepository, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, private val visaAuthTokenStorage: VisaAuthTokenStorage, ) { @@ -83,7 +83,7 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting challenge for wallet authorization") - val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge( + val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge( cardId = card.cardId, // This is the wallet public key, not the address and it's alright, as the API expects it in this format cardWalletAddress = wallet.publicKey.toHexString(), @@ -122,7 +122,7 @@ internal class VisaCardScanHandler @Inject constructor( cardWalletAddress: String, signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { - val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) + val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge) .getOrElse { Timber.i("Failed to get Access token for Wallet public key authorization.") return if ( @@ -149,7 +149,7 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting authorization challenge to sign") - val challengeResponse = visaAuthRepository.getCardAuthChallenge( + val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge( cardId = card.cardId, cardPublicKey = card.cardPublicKey.toHexString(), ).getOrElse { @@ -174,7 +174,7 @@ internal class VisaCardScanHandler @Inject constructor( } } - val authorizationTokensResponse = visaAuthRepository.getAccessTokens( + val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens( signedChallenge = challengeResponse.toSignedChallenge( signedChallenge = attestCardKeyResponse.cardSignature.toHexString(), salt = attestCardKeyResponse.salt.toHexString(), diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 8678e1959c..7549cc097f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -103,4 +103,7 @@ interface TangemPayApi { @GET("v1/customer/kyc") suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse + + @GET("v1/customer/me") + suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt new file mode 100644 index 0000000000..6814d094a9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -0,0 +1,48 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CustomerMeResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "id") val id: String, + @Json(name = "state") val state: String, + @Json(name = "createdAt") val createdAt: String, + @Json(name = "product_instance") val productInstance: ProductInstance, + @Json(name = "payment_account") val paymentAccount: PaymentAccount, + @Json(name = "kyc") val kyc: Kyc, + ) + + @JsonClass(generateAdapter = true) + data class ProductInstance( + @Json(name = "id") val id: String, + @Json(name = "cid") val cid: String, + @Json(name = "card_id") val cardId: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "status") val status: String, + @Json(name = "updated_at") val updatedAt: String, + @Json(name = "payment_account_id") val paymentAccountId: String, + ) + + @JsonClass(generateAdapter = true) + data class PaymentAccount( + @Json(name = "id") val id: String, + @Json(name = "address") val address: String, + @Json(name = "customer_wallet_address") val customerWalletAddress: String, + ) + + @JsonClass(generateAdapter = true) + data class Kyc( + @Json(name = "id") val id: String, + @Json(name = "provider") val provider: String, + @Json(name = "status") val status: String, + @Json(name = "risk") val risk: String, + @Json(name = "review_answer") val reviewAnswer: String, + @Json(name = "created_at") val createdAt: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt new file mode 100644 index 0000000000..d1f8d27e19 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.visa + +interface TangemPayStorage { + + suspend fun store(authHeader: String) + + suspend fun get(): String? + + suspend fun clear() +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt new file mode 100644 index 0000000000..80ade6e92b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -0,0 +1,45 @@ +package com.tangem.data.pay.datasource + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.common.CompletionResult +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.sdk.api.TangemSdkManager +import javax.inject.Inject + +internal class DefaultTangemPayAuthDataSource @Inject constructor( + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, + private val tangemSdkManager: TangemSdkManager, +) : TangemPayAuthDataSource { + + override suspend fun generateNewAuthHeader(address: String, cardId: String): Either = either { + val challenge = visaAuthRemoteDataSource + .getCustomerWalletAuthChallenge(address) + .mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") } + .bind() + + val signed = tangemSdkManager.visaCustomerWalletApprove( + VisaDataForApprove( + customerWalletCardId = cardId, + targetAddress = address, + dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge), + ), + ).toEither { IllegalStateException("TangemPay signing failed: $it") }.bind() + + visaAuthRemoteDataSource.getTokenWithCustomerWallet( + sessionId = challenge.session.sessionId, + signature = signed.signature, + nonce = signed.dataToSign.hashToSign, + ) + .mapLeft { IllegalStateException("TangemPay token fetch failed. Error code: ${it.errorCode}") } + .bind() + } +} + +private fun CompletionResult.toEither(map: (Throwable) -> Throwable) = when (this) { + is CompletionResult.Success -> Either.Right(data) + is CompletionResult.Failure -> Either.Left(map(error)) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index d0de7d5c31..727e31f796 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,6 +1,6 @@ package com.tangem.data.pay.di -import com.tangem.data.pay.DefaultKycRepository +import com.tangem.data.pay.repository.DefaultKycRepository import com.tangem.domain.pay.repository.KycRepository import dagger.Binds import dagger.Module diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt similarity index 60% rename from data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt rename to data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt index cc1a69895e..5ecfd916c5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt @@ -1,58 +1,48 @@ -package com.tangem.data.pay +package com.tangem.data.pay.repository import arrow.core.Either import com.squareup.moshi.Moshi -import com.tangem.common.map import com.tangem.core.error.UniversalError import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet -import com.tangem.domain.visa.repository.VisaAuthRepository -import com.tangem.sdk.api.TangemSdkManager import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject class DefaultKycRepository @AssistedInject constructor( @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, - private val visaAuthRepository: VisaAuthRepository, - private val tangemSdkManager: TangemSdkManager, + private val authDataSource: TangemPayAuthDataSource, + private val tangemPayStorage: TangemPayStorage, ) : KycRepository { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) override suspend fun getKycStartInfo(address: String, cardId: String): Either { - var authHeader = "" - visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result -> - tangemSdkManager.visaCustomerWalletApprove( - VisaDataForApprove( - customerWalletCardId = cardId, - targetAddress = address, - dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge), - ), - ).map { signResult -> - visaAuthRepository.getTokenWithCustomerWallet( - sessionId = result.session.sessionId, - signature = signResult.signature, - nonce = signResult.dataToSign.hashToSign, - ).getOrNull()?.let { authHeader = it } - } - } + val authHeader = authDataSource.generateNewAuthHeader(address, cardId) + .getOrNull() + .takeIf { !it.isNullOrEmpty() } + ?: return Either.Left(VisaApiError.UnknownWithoutCode) + tangemPayStorage.store(authHeader) + return getKycInfo(authHeader) + } + + override suspend fun getKycStartInfo(authHeader: String): Either { + return getKycInfo(authHeader) + } + + private suspend fun getKycInfo(authHeader: String): Either { return request { - authHeader.ifEmpty { error("Cannot get auth header for KYC") } tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result }.map { - KycStartInfo( - token = it.token, - locale = it.locale, - ) + KycStartInfo(token = it.token, locale = it.locale) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index b38b9a82a8..939794afce 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -18,7 +18,7 @@ import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -32,7 +32,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( private val visaApi: TangemPayApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaAuthTokenStorage: VisaAuthTokenStorage, - private val visaAuthRepository: VisaAuthRepository, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, private val visaLibLoader: VisaLibLoader, private val apiConfigsManager: ApiConfigsManager, ) : VisaActivationRepository { @@ -194,7 +194,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( } val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored") - val newTokens = visaAuthRepository.refreshAccessTokens(authTokens.refreshToken).getOrElse { + val newTokens = visaAuthRemoteDataSource.refreshAccessTokens(authTokens.refreshToken).getOrElse { return Either.Left(VisaApiError.RefreshTokenExpired) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt similarity index 97% rename from data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt rename to data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt index 8b3024f1fe..e396978597 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt @@ -13,17 +13,16 @@ import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSession import com.tangem.domain.visa.model.VisaAuthSignedChallenge import com.tangem.domain.visa.model.VisaAuthTokens -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject -@Suppress("UnusedPrivateMember") -internal class DefaultVisaAuthRepository @Inject constructor( +internal class DefaultVisaAuthRemoteDataSource @Inject constructor( @NetworkMoshi private val moshi: Moshi, private val visaAuthApi: TangemPayApi, private val dispatchers: CoroutineDispatcherProvider, -) : VisaAuthRepository { +) : VisaAuthRemoteDataSource { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a173f8c5ea..a5efc3d025 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,10 +1,12 @@ package com.tangem.data.visa.di +import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource import com.tangem.data.visa.DefaultVisaActivationRepository -import com.tangem.data.visa.DefaultVisaAuthRepository +import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource import com.tangem.data.visa.MockVisaRepository +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.repository.VisaRepository import dagger.Binds import dagger.Module @@ -18,7 +20,7 @@ internal interface VisaDataModule { @Binds @Singleton - fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository + fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource @Binds @Singleton @@ -39,4 +41,8 @@ internal interface VisaDataModule { // Mocked @Binds fun bindVisaRepository(repository: MockVisaRepository): VisaRepository + + @Binds + @Singleton + fun bindTangemPayAuthDataSource(repository: DefaultTangemPayAuthDataSource): TangemPayAuthDataSource } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt new file mode 100644 index 0000000000..323d779515 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.datasource + +import arrow.core.Either + +interface TangemPayAuthDataSource { + + suspend fun generateNewAuthHeader(address: String, cardId: String): Either +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index 7d46ff2d52..5d4ef99fd3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -6,8 +6,16 @@ import com.tangem.domain.pay.KycStartInfo interface KycRepository { + /** + * Returns fresh KYC data to start the survey. Used only for first time launch + */ suspend fun getKycStartInfo(address: String, cardId: String): Either + /** + * Returns KYC data to continue the survey. Used when KYC wasn't finished by the user + */ + suspend fun getKycStartInfo(authHeader: String): Either + interface Factory { fun create(): KycRepository } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt similarity index 93% rename from domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt index 098ca44c00..ebec77de6b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.visa.repository +package com.tangem.domain.visa.datasource import arrow.core.Either import com.tangem.domain.visa.error.VisaApiError @@ -6,7 +6,7 @@ import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSignedChallenge import com.tangem.domain.visa.model.VisaAuthTokens -interface VisaAuthRepository { +interface VisaAuthRemoteDataSource { suspend fun getCardAuthChallenge( cardId: String, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 15c3b08a41..b80bb62fda 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent @@ -44,7 +44,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @Suppress("UnusedPrivateMember") private val tangemSdkManager: TangemSdkManager, - private val visaAuthRepository: VisaAuthRepository, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, private val uiMessageSender: UiMessageSender, private val analyticsEventsHandler: AnalyticsEventHandler, ) : Model() { @@ -155,7 +155,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( loading(true) modelScope.launch { - val challengeToSign = visaAuthRepository.getCardAuthChallenge( + val challengeToSign = visaAuthRemoteDataSource.getCardAuthChallenge( cardId = activationInput.cardId, cardPublicKey = activationInput.cardPublicKey, ).getOrElse { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index e517a628cb..1786357959 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -42,7 +42,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( paramsContainer: ParamsContainer, visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, - private val visaAuthRepository: VisaAuthRepository, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, @@ -166,7 +166,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val authTokens = visaAuthTokenStorage.get(params.scanResponse.card.cardId) ?: error("Auth tokens are not found. This should not happen.") - val newTokens = visaAuthRepository.exchangeAccessToken(authTokens) + val newTokens = visaAuthRemoteDataSource.exchangeAccessToken(authTokens) .getOrElse { uiMessageSender.showErrorDialog(it) return From d34a83ced5d9137aa76cdf5546be78a32d7108d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 15:50:42 +0200 Subject: [PATCH 39/48] Updated on 2026-08-14 --- domain/onramp/build.gradle.kts | 11 + .../tangem/domain/onramp/model/OnrampOffer.kt | 20 ++ .../onramp/model/OnrampPaymentMethod.kt | 36 ++- .../onramp/model/OnrampPaymentMethodGroup.kt | 19 ++ .../onramp/GetOnrampAllOffersUseCase.kt | 76 +++++ .../domain/onramp/GetOnrampOffersUseCase.kt | 197 ++++++++++++ .../onramp/GetOnrampAllOffersUseCaseTest.kt | 177 +++++++++++ .../onramp/GetOnrampOffersUseCaseTest.kt | 280 ++++++++++++++++++ 8 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt create mode 100644 domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt create mode 100644 domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt create mode 100644 domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index da56879693..7823e8dcea 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -4,6 +4,10 @@ plugins { id("configuration") } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Core modules */ implementation(projects.core.analytics.models) @@ -15,4 +19,11 @@ dependencies { api(projects.domain.core) api(projects.domain.settings) implementation(deps.kotlin.serialization) + + /** Tests */ + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt new file mode 100644 index 0000000000..e116e55735 --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.onramp.model + +data class OnrampOffersBlock( + val category: OnrampOfferCategory, + val offers: List, + val isVisible: Boolean = offers.isNotEmpty(), +) + +data class OnrampOffer( + val quote: OnrampQuote, + val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default, +) + +enum class OnrampOfferAdvantages { + Default, BestRate, Fastest, +} + +enum class OnrampOfferCategory { + Recent, Recommended, +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt index a786bb843b..17c2e41ae0 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt @@ -13,27 +13,59 @@ data class OnrampPaymentMethod( enum class PaymentMethodType(val id: String?) { GOOGLE_PAY(id = "google-pay"), CARD(id = "card"), + REVOLUT_PAY(id = "invoice-revolut-pay"), + SEPA(id = "sepa"), OTHER(id = null), ; + @Suppress("MagicNumber") fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) { when (this) { GOOGLE_PAY -> 0 CARD -> 1 - OTHER -> 2 + SEPA -> 2 + REVOLUT_PAY -> 3 + OTHER -> 4 } } else { when (this) { CARD -> 0 GOOGLE_PAY -> 1 - OTHER -> 2 + SEPA -> 2 + REVOLUT_PAY -> 3 + OTHER -> 4 } } + /** + * BE AWARE. HARDCODED. Returns the speed of transaction for payment method type. + */ + fun getProcessingSpeed(): PaymentSpeed = when (this) { + REVOLUT_PAY, + GOOGLE_PAY, + -> PaymentSpeed.Instant + CARD -> PaymentSpeed.FewMin + SEPA -> PaymentSpeed.FewDays + OTHER -> PaymentSpeed.PlentyDays + } + + /** + * @param speed - the lower the value, the faster the speed. + */ + @Suppress("MagicNumber") + enum class PaymentSpeed(val speed: Int) { + Instant(0), FewMin(1), FewDays(2), PlentyDays(3), Unknown(4) + } + + fun isInstant(): Boolean = getProcessingSpeed() == PaymentSpeed.Instant + companion object { + fun getType(id: String): PaymentMethodType = when (id) { GOOGLE_PAY.id -> GOOGLE_PAY CARD.id -> CARD + REVOLUT_PAY.id -> REVOLUT_PAY + SEPA.id -> SEPA else -> OTHER } } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt new file mode 100644 index 0000000000..9a957d44ff --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp.model + +import java.math.BigDecimal + +data class OnrampPaymentMethodGroup( + val paymentMethod: OnrampPaymentMethod, + val offers: List, + val bestRateOffer: OnrampOffer?, + val providerCount: Int, + val isBestPaymentMethod: Boolean, +) { + + val bestRateAmount: BigDecimal? = bestRateOffer?.let { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt new file mode 100644 index 0000000000..6cc1247867 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt @@ -0,0 +1,76 @@ +package com.tangem.domain.onramp + +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.OnrampOffer +import com.tangem.domain.onramp.model.OnrampOfferAdvantages +import com.tangem.domain.onramp.model.OnrampPaymentMethodGroup +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.settings.repositories.SettingsRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.math.BigDecimal + +class GetOnrampAllOffersUseCase( + private val onrampRepository: OnrampRepository, + private val errorResolver: OnrampErrorResolver, + private val settingsRepository: SettingsRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): EitherFlow> { + return onrampRepository.getQuotes() + .map { quotes -> processAllOffers(quotes).right() } + .catch { throwable -> errorResolver.resolve(throwable).left() } + } + + private suspend fun processAllOffers(quotes: List): List { + val validQuotes = quotes.filterIsInstance() + if (validQuotes.isEmpty()) return emptyList() + val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() + + val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + + val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } + + return offersByPaymentMethod.map { (paymentMethod, methodQuotes) -> + val methodOffers = methodQuotes.map { quote -> + val advantages = if (quote == overallBestRateQuote) { + OnrampOfferAdvantages.BestRate + } else { + OnrampOfferAdvantages.Default + } + OnrampOffer(quote = quote, advantages = advantages) + } + + val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } + val groupBestRateOffer = methodOffers.find { + when (val quote = it.quote) { + is OnrampQuote.Data -> quote == groupBestRateOfferData + else -> false + } + } + + OnrampPaymentMethodGroup( + paymentMethod = paymentMethod, + offers = methodOffers.sortedByDescending { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + }, + providerCount = methodOffers.map { it.quote.provider.id }.distinct().size, + bestRateOffer = groupBestRateOffer, + isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod, + ) + }.sortedBy { it.paymentMethod.type.getPriority(isGooglePayAvailable) } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt new file mode 100644 index 0000000000..29041336a9 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -0,0 +1,197 @@ +package com.tangem.domain.onramp + +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import java.math.BigDecimal + +class GetOnrampOffersUseCase( + private val onrampRepository: OnrampRepository, + private val onrampTransactionRepository: OnrampTransactionRepository, + private val errorResolver: OnrampErrorResolver, +) { + + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): EitherFlow> { + return combine( + onrampRepository.getQuotes(), + onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId), + ) { quotes, transactions -> + processOffers(quotes, transactions) + } + .map { offers -> offers.right() } + .catch { throwable -> errorResolver.resolve(throwable).left() } + } + + private fun processOffers( + quotes: List, + transactions: List, + ): List { + val validQuotes = quotes.filterIsInstance() + if (validQuotes.isEmpty()) return emptyList() + + val offers = validQuotes.map { quote -> + OnrampOffer(quote = quote) + } + + val recentOffer = findRecentOffer(offers, transactions) + val bestRateOffer = findBestRateOffer(offers) + val fastestOffer = findFastestOffer(offers) + + return buildOffersBlocks( + recentOffer = recentOffer, + bestRateOffer = bestRateOffer, + fastestOffer = fastestOffer, + allOffers = offers, + ) + } + + private fun findRecentOffer(offers: List, transactions: List): OnrampOffer? { + val lastTransaction = transactions.maxByOrNull { it.timestamp } ?: return null + + return offers.find { offer -> + offer.quote.provider.id == lastTransaction.providerType && + offer.quote.paymentMethod.id == lastTransaction.paymentMethod + } + } + + private fun findBestRateOffer(offers: List): OnrampOffer? { + return offers.maxByOrNull { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } + } + + private fun findFastestOffer(offers: List): OnrampOffer? { + val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } + return if (instantOffers.isNotEmpty()) { + instantOffers.maxByOrNull { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } + } else { + val offersBySpeed = offers.groupBy { offer -> + offer.quote.paymentMethod.type.getProcessingSpeed().speed + } + val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null + val fastestOffers = offersBySpeed[fastestSpeed] ?: return null + fastestOffers.maxByOrNull { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } + } + } + + private fun buildOffersBlocks( + recentOffer: OnrampOffer?, + bestRateOffer: OnrampOffer?, + fastestOffer: OnrampOffer?, + allOffers: List, + ): List { + return buildList { + if (recentOffer != null) { + add( + OnrampOffersBlock( + category = OnrampOfferCategory.Recent, + offers = listOf( + recentOffer.copy( + advantages = determineAdvantages( + recentOffer, + bestRateOffer, + fastestOffer, + ), + ), + ), + ), + ) + } + + val recommendedOffers = buildRecommendedOffers( + recentOffer = recentOffer, + bestRateOffer = bestRateOffer, + fastestOffer = fastestOffer, + ) + + if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) { + add( + OnrampOffersBlock( + category = OnrampOfferCategory.Recommended, + offers = recommendedOffers, + ), + ) + } + } + } + + private fun determineAdvantages( + recentOffer: OnrampOffer, + bestRateOffer: OnrampOffer?, + fastestOffer: OnrampOffer?, + ): OnrampOfferAdvantages { + if (isSameOffer(recentOffer, bestRateOffer) && isSameOffer(recentOffer, fastestOffer)) { + return OnrampOfferAdvantages.BestRate + } + if (isSameOffer(recentOffer, bestRateOffer)) { + return OnrampOfferAdvantages.BestRate + } + if (isSameOffer(recentOffer, fastestOffer)) { + return OnrampOfferAdvantages.Fastest + } + return OnrampOfferAdvantages.Default + } + + private fun buildRecommendedOffers( + recentOffer: OnrampOffer?, + bestRateOffer: OnrampOffer?, + fastestOffer: OnrampOffer?, + ): List { + return buildList { + if (isSameOffer(bestRateOffer, fastestOffer)) { + bestRateOffer?.let { offer -> + add(offer.copy(advantages = OnrampOfferAdvantages.BestRate)) + } + } else { + if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) { + add(bestRateOffer.copy(advantages = OnrampOfferAdvantages.BestRate)) + } + + if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) && + !isSameOffer(fastestOffer, bestRateOffer) + ) { + add(fastestOffer.copy(advantages = OnrampOfferAdvantages.Fastest)) + } + } + } + } + + private fun hasOnlyOneMethodAndProvider(offers: List): Boolean { + val uniquePaymentMethods = offers.map { it.quote.paymentMethod.id }.distinct() + val uniqueProviders = offers.map { it.quote.provider.id }.distinct() + return uniquePaymentMethods.size == 1 && uniqueProviders.size == 1 + } + + private fun isSameOffer(offer1: OnrampOffer?, offer2: OnrampOffer?): Boolean { + if (offer1 == null || offer2 == null) return false + return offer1.quote.provider.id == offer2.quote.provider.id && + offer1.quote.paymentMethod.id == offer2.quote.paymentMethod.id + } +} \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt new file mode 100644 index 0000000000..89c4b8e6f9 --- /dev/null +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt @@ -0,0 +1,177 @@ +package com.tangem.domain.onramp + +import com.google.common.truth.Truth +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.OnrampOfferAdvantages +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProvider +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.settings.repositories.SettingsRepository +import io.mockk.* +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetOnrampAllOffersUseCaseTest { + + private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true) + private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) + private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) + private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val userWalletId: UserWalletId = mockk(relaxUnitFun = true) + + private lateinit var useCase: GetOnrampAllOffersUseCase + + @BeforeEach + fun setup() { + clearMocks(onrampRepository, errorResolver, settingsRepository, cryptoCurrencyId) + useCase = GetOnrampAllOffersUseCase( + onrampRepository = onrampRepository, + errorResolver = errorResolver, + settingsRepository = settingsRepository, + ) + } + + @Test + fun `invoke should return empty list when no valid quotes`() = runTest { + val emptyQuotes = listOf() + coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> Truth.assertThat(offers).isEmpty() }, + ) + } + coVerify { onrampRepository.getQuotes() } + } + + @Test + fun `invoke should return grouped offers with best rate marked`() = runTest { + val paymentMethod1 = createMockPaymentMethod("card", "Card") + val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer") + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockQuote(paymentMethod1, provider1, BigDecimal("100.0")), + createMockQuote(paymentMethod1, provider2, BigDecimal("95.0")), + createMockQuote(paymentMethod2, provider1, BigDecimal("98.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(2) + + val cardGroup = offers.find { it.paymentMethod.id == "card" } + Truth.assertThat(cardGroup).isNotNull() + Truth.assertThat(cardGroup?.offers).hasSize(2) + Truth.assertThat(cardGroup?.providerCount).isEqualTo(2) + Truth.assertThat(cardGroup?.isBestPaymentMethod).isTrue() + + val bestRateOffer = cardGroup?.offers?.find { it.advantages == OnrampOfferAdvantages.BestRate } + Truth.assertThat(bestRateOffer).isNotNull() + + val bankGroup = offers.find { it.paymentMethod.id == "bank" } + Truth.assertThat(bankGroup).isNotNull() + Truth.assertThat(bankGroup?.offers).hasSize(1) + Truth.assertThat(bankGroup?.providerCount).isEqualTo(1) + Truth.assertThat(bankGroup?.isBestPaymentMethod).isFalse() + }, + ) + } + + coVerify { onrampRepository.getQuotes() } + coVerify { settingsRepository.isGooglePayAvailability() } + } + + @Test + fun `invoke should sort offers by toAmount descending`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card") + val provider = createMockProvider("provider1", "Provider 1") + + val quotes = listOf( + createMockQuote(paymentMethod, provider, BigDecimal("90.0")), + createMockQuote(paymentMethod, provider, BigDecimal("100.0")), + createMockQuote(paymentMethod, provider, BigDecimal("95.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + val group = offers.first() + Truth.assertThat(group.offers).hasSize(3) + + val amounts = group.offers.map { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } + Truth.assertThat(amounts).containsExactly( + BigDecimal("100.0"), + BigDecimal("95.0"), + BigDecimal("90.0"), + ).inOrder() + }, + ) + } + } + + private fun createMockPaymentMethod(id: String, name: String): OnrampPaymentMethod { + return mockk { + every { this@mockk.id } returns id + every { this@mockk.name } returns name + every { this@mockk.type } returns mockk { + every { getPriority(any()) } returns 1 + } + } + } + + private fun createMockProvider(id: String, name: String): OnrampProvider { + return mockk { + every { this@mockk.id } returns id + every { this@mockk.info.name } returns name + } + } + + private fun createMockQuote( + paymentMethod: OnrampPaymentMethod, + provider: OnrampProvider, + toAmount: BigDecimal, + ): OnrampQuote.Data { + return mockk { + every { this@mockk.paymentMethod } returns paymentMethod + every { this@mockk.provider } returns provider + every { this@mockk.toAmount } returns mockk { + every { value } returns toAmount + } + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt new file mode 100644 index 0000000000..62b55b230a --- /dev/null +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -0,0 +1,280 @@ +package com.tangem.domain.onramp + +import com.google.common.truth.Truth +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import io.mockk.* +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetOnrampOffersUseCaseTest { + + private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true) + private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true) + private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) + private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val userWalletId: UserWalletId = mockk(relaxUnitFun = true) + + private lateinit var useCase: GetOnrampOffersUseCase + + @BeforeEach + fun setup() { + clearMocks(onrampRepository, onrampTransactionRepository, errorResolver, cryptoCurrencyId) + useCase = GetOnrampOffersUseCase( + onrampRepository = onrampRepository, + onrampTransactionRepository = onrampTransactionRepository, + errorResolver = errorResolver, + ) + } + + @Test + fun `invoke should return empty list when no valid quotes`() = runTest { + val emptyQuotes = listOf() + val emptyTransactions = listOf() + + coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) + coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( + emptyTransactions, + ) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> Truth.assertThat(offers).isEmpty() }, + ) + } + + coVerify { onrampRepository.getQuotes() } + coVerify { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } + } + + @Test + fun `invoke should return offers blocks with recent and recommended categories`() = runTest { + val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = true) + val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")), + createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")), + ) + + val transactions = listOf( + createMockTransaction("provider1", "card", 1000L), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( + transactions, + ) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(2) + + val recentBlock = offers.find { it.category == OnrampOfferCategory.Recent } + Truth.assertThat(recentBlock).isNotNull() + Truth.assertThat(recentBlock?.offers).hasSize(1) + Truth.assertThat(recentBlock?.offers?.first()?.advantages).isEqualTo(OnrampOfferAdvantages.Fastest) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(1) + Truth.assertThat(recommendedBlock?.offers?.first()?.advantages) + .isEqualTo(OnrampOfferAdvantages.BestRate) + }, + ) + } + } + + @Test + fun `invoke should find best rate offer correctly`() = runTest { + val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = false) + val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")), + createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")), + createMockQuote(paymentMethod1, provider1, BigDecimal("95.0")), + ) + + val transactions = emptyList() + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( + transactions, + ) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(1) + + val bestRateOffer = recommendedBlock?.offers?.first() + Truth.assertThat(bestRateOffer?.advantages).isEqualTo(OnrampOfferAdvantages.BestRate) + + when (val quote = bestRateOffer?.quote) { + is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should find fastest offer correctly`() = runTest { + val instantPaymentMethod = createMockPaymentMethod("card", "Card", isInstant = true) + val slowPaymentMethod = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockQuote(instantPaymentMethod, provider1, BigDecimal("90.0")), + createMockQuote(slowPaymentMethod, provider2, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( + transactions, + ) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(2) + + val bestRateOffer = recommendedBlock + ?.offers + ?.find { it.advantages == OnrampOfferAdvantages.BestRate } + val fastestOffer = recommendedBlock + ?.offers + ?.find { it.advantages == OnrampOfferAdvantages.Fastest } + + Truth.assertThat(bestRateOffer).isNotNull() + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = bestRateOffer?.quote) { + is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + else -> Truth.assertThat(false).isTrue() + } + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("90.0")) + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should not show recommended block when only one method and provider`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", isInstant = false) + val provider = createMockProvider("provider1", "Provider 1") + + val quotes = listOf( + createMockQuote(paymentMethod, provider, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( + transactions, + ) + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).isEmpty() + }, + ) + } + } + + private fun createMockPaymentMethod(id: String, name: String, isInstant: Boolean): OnrampPaymentMethod { + return mockk { + every { this@mockk.id } returns id + every { this@mockk.name } returns name + every { this@mockk.type } returns mockk { + every { isInstant() } returns isInstant + every { getProcessingSpeed() } returns mockk { + every { speed } returns if (isInstant) 1 else 3 + } + } + } + } + + private fun createMockProvider(id: String, name: String): OnrampProvider { + return mockk { + every { this@mockk.id } returns id + every { this@mockk.info.name } returns name + } + } + + private fun createMockQuote( + paymentMethod: OnrampPaymentMethod, + provider: OnrampProvider, + toAmount: BigDecimal, + ): OnrampQuote.Data { + return mockk { + every { this@mockk.paymentMethod } returns paymentMethod + every { this@mockk.provider } returns provider + every { this@mockk.toAmount } returns mockk { + every { value } returns toAmount + } + } + } + + private fun createMockTransaction(providerType: String, paymentMethod: String, timestamp: Long): OnrampTransaction { + return mockk { + every { this@mockk.providerType } returns providerType + every { this@mockk.paymentMethod } returns paymentMethod + every { this@mockk.timestamp } returns timestamp + } + } +} \ No newline at end of file From 498e62329a202d34b961633dcf7d1869bf0c0db8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 15:51:42 +0200 Subject: [PATCH 40/48] Updated on 2026-08-14 --- .../mainv2/DefaultOnrampV2MainComponent.kt | 79 ++++ .../DefaultOnrampV2MainFeatureToggle.kt} | 6 +- .../OnrampV2MainComponent.kt} | 6 +- .../mainv2/OnrampV2MainFeatureToggle.kt | 5 + .../di/OnrampMainV2ComponentModelModule.kt | 20 + .../mainv2/di/OnrampNewV2ComponentModule.kt | 33 ++ .../mainv2/entity/OnrampOfferBlockUM.kt | 41 ++ .../mainv2/entity/OnrampV2AmountBlockUM.kt | 38 ++ .../entity/OnrampV2MainBottomSheetConfig.kt | 13 + .../mainv2/entity/OnrampV2MainComponentUM.kt | 43 +++ .../mainv2/entity/OnrampV2ProvidersUM.kt | 15 + .../OnrampV2AmountFieldChangeConverter.kt | 84 ++++ .../OnrampAmountButtonUMStateFactory.kt | 35 ++ .../factory/OnrampV2AmountStateFactory.kt | 222 +++++++++++ .../entity/factory/OnrampV2StateFactory.kt | 205 ++++++++++ .../model/OnrampV2MainComponentModel.kt | 359 ++++++++++++++++++ .../onramp/mainv2/ui/OnrampFooterContent.kt | 130 +++++++ .../ui/OnrampNewMainComponentContent.kt | 141 +++++++ .../onramp/mainv2/ui/OnrampOffersContent.kt | 32 ++ .../onramp/mainv2/ui/OnrampV2AmountContent.kt | 199 ++++++++++ .../newmain/OnrampNewMainFeatureToggle.kt | 5 - 21 files changed, 1700 insertions(+), 11 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{newmain/DefaultOnrampNewMainFeatureToggle.kt => mainv2/DefaultOnrampV2MainFeatureToggle.kt} (69%) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{newmain/OnrampNewMainComponent.kt => mainv2/OnrampV2MainComponent.kt} (76%) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt new file mode 100644 index 0000000000..a651416d7f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt @@ -0,0 +1,79 @@ +package com.tangem.features.onramp.mainv2 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig +import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel +import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen +import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampV2MainComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: OnrampV2MainComponent.Params, + private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, + private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, +) : OnrampV2MainComponent, AppComponentContext by appComponentContext { + + private val model: OnrampV2MainComponentModel = getOrCreateModel(params) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsState() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + OnrampNewMainScreen(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: OnrampV2MainBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( + context = childByContext(componentContext), + params = ConfirmResidencyComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + country = config.country, + onDismiss = { model.bottomSheetNavigation.dismiss() }, + ), + ) + is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( + context = childByContext(componentContext), + params = SelectCurrencyComponent.Params( + userWallet = model.userWallet, + cryptoCurrency = params.cryptoCurrency, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : OnrampV2MainComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampV2MainComponent.Params, + ): DefaultOnrampV2MainComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt similarity index 69% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt index e73a8570bd..256bead28d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/DefaultOnrampNewMainFeatureToggle.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt @@ -1,10 +1,10 @@ -package com.tangem.features.onramp.newmain +package com.tangem.features.onramp.mainv2 import com.tangem.core.configtoggle.feature.FeatureTogglesManager -class DefaultOnrampNewMainFeatureToggle( +class DefaultOnrampV2MainFeatureToggle( private val featureTogglesManager: FeatureTogglesManager, -) : OnrampNewMainFeatureToggle { +) : OnrampV2MainFeatureToggle { override val isOnrampNewMainEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED") } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt similarity index 76% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt index d30d134f81..9767cf4496 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.newmain +package com.tangem.features.onramp.mainv2 import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent @@ -7,7 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampSource -internal interface OnrampNewMainComponent : ComposableContentComponent { +internal interface OnrampV2MainComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, @@ -17,5 +17,5 @@ internal interface OnrampNewMainComponent : ComposableContentComponent { val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, ) - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt new file mode 100644 index 0000000000..54595ff8d7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onramp.mainv2 + +internal interface OnrampV2MainFeatureToggle { + val isOnrampNewMainEnabled: Boolean +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt new file mode 100644 index 0000000000..6d20f87cf7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.mainv2.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface OnrampMainV2ComponentModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampV2MainComponentModel::class) + fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt new file mode 100644 index 0000000000..08817d31ac --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onramp.mainv2.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent +import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle +import com.tangem.features.onramp.mainv2.OnrampV2MainComponent +import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampNewMainComponentModule { + + @Binds + @Singleton + fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory +} + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureToggleModule { + + @Provides + @Singleton + fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle { + return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt new file mode 100644 index 0000000000..583cc962bb --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt @@ -0,0 +1,41 @@ +package com.tangem.features.onramp.mainv2.entity + +import androidx.compose.runtime.Immutable +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed interface OnrampOffersBlockUM { + + val isBlockVisible: Boolean + + data object Empty : OnrampOffersBlockUM { + override val isBlockVisible: Boolean + get() = false + } + + data class Loading( + override val isBlockVisible: Boolean, + ) : OnrampOffersBlockUM + + data class Content( + override val isBlockVisible: Boolean, + val offers: ImmutableList, + ) : OnrampOffersBlockUM +} + +internal data class OnrampOfferUM( + val category: OnrampOfferCategory, + val advantages: OnrampOfferAdvantages, + val paymentMethod: OnrampPaymentMethod, + val providerId: String, + val providerName: String, +) + +internal enum class OnrampOfferCategory { + RecentlyUsed, Recommended +} + +internal enum class OnrampOfferAdvantages { + Default, BestRate, Fastest +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt new file mode 100644 index 0000000000..b151b7818c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.mainv2.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class OnrampNewAmountBlockUM( + val currencyUM: OnrampNewCurrencyUM, + val amountFieldModel: AmountFieldModel, + val secondaryFieldModel: OnrampNewAmountSecondaryFieldUM, +) + +internal data class OnrampNewCurrencyUM( + val unit: String, + val code: String, + val iconUrl: String?, + val precision: Int, + val onClick: () -> Unit, +) + +@Immutable +internal sealed interface OnrampNewAmountSecondaryFieldUM { + data object Loading : OnrampNewAmountSecondaryFieldUM + data class Content(val amount: TextReference) : OnrampNewAmountSecondaryFieldUM + data class Error(val error: TextReference) : OnrampNewAmountSecondaryFieldUM +} + +internal sealed interface OnrampV2AmountButtonUMState { + data class Loaded(val amountButtons: ImmutableList) : OnrampV2AmountButtonUMState + data object None : OnrampV2AmountButtonUMState +} + +internal data class OnrampAmountButtonUM( + val value: Int, + val currency: String, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt new file mode 100644 index 0000000000..dcb21bbf8a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onramp.mainv2.entity + +import com.tangem.domain.onramp.model.OnrampCountry +import kotlinx.serialization.Serializable + +@Serializable +sealed interface OnrampV2MainBottomSheetConfig { + @Serializable + data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig + + @Serializable + data object CurrenciesList : OnrampV2MainBottomSheetConfig +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt new file mode 100644 index 0000000000..0b6560e380 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt @@ -0,0 +1,43 @@ +package com.tangem.features.onramp.mainv2.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed interface OnrampV2MainComponentUM { + + val topBarConfig: OnrampV2MainTopBarUM + val continueButtonConfig: ContinueButtonUM + val errorNotification: NotificationUM? + + data class InitialLoading( + override val topBarConfig: OnrampV2MainTopBarUM, + override val continueButtonConfig: ContinueButtonUM, + override val errorNotification: NotificationUM?, + ) : OnrampV2MainComponentUM + + data class Content( + override val topBarConfig: OnrampV2MainTopBarUM, + override val continueButtonConfig: ContinueButtonUM, + override val errorNotification: NotificationUM?, + val amountBlockState: OnrampNewAmountBlockUM, + val offersBlockState: OnrampOffersBlockUM, + val onrampAmountButtonUMState: OnrampV2AmountButtonUMState, + val onrampProviderState: OnrampV2ProvidersUM, + ) : OnrampV2MainComponentUM +} + +internal data class ContinueButtonUM( + val text: TextReference, + val onClick: () -> Unit, + val enabled: Boolean, + val showProgress: Boolean = false, +) + +internal data class OnrampV2MainTopBarUM( + val title: TextReference, + val startButtonUM: TopAppBarButtonUM, + val endButtonUM: TopAppBarButtonUM, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt new file mode 100644 index 0000000000..750e9ba8f3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.mainv2.entity + +import com.tangem.domain.onramp.model.OnrampPaymentMethod + +sealed interface OnrampV2ProvidersUM { + + data object Empty : OnrampV2ProvidersUM + + data object Loading : OnrampV2ProvidersUM + + data class Content( + val providerId: String, + val paymentMethod: OnrampPaymentMethod, + ) : OnrampV2ProvidersUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt new file mode 100644 index 0000000000..2f8f0f4186 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt @@ -0,0 +1,84 @@ +package com.tangem.features.onramp.mainv2.entity.converter + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.parseBigDecimalOrNull +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.onramp.main.entity.OnrampIntents +import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class OnrampV2AmountFieldChangeConverter( + private val currentStateProvider: Provider, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, + private val onrampIntents: OnrampIntents, + private val cryptoCurrency: CryptoCurrency, +) : Converter { + + override fun convert(value: String): OnrampV2MainComponentUM { + val state = currentStateProvider() + if (state !is OnrampV2MainComponentUM.Content) return state + + if (value.isEmpty()) return state.emptyState() + + val amountState = state.amountBlockState + val amountTextField = amountState.amountFieldModel + val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO + val amountFieldModel = amountState.amountFieldModel.copy( + fiatValue = value, + fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + + return state.copy( + amountBlockState = amountState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading, + ), + continueButtonConfig = state.continueButtonConfig.copy(enabled = false), + onrampProviderState = OnrampV2ProvidersUM.Loading, + onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + ) + } + + private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content { + val amountFieldModel = amountBlockState.amountFieldModel.copy( + value = "", + fiatValue = "", + cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), + isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ) + return copy( + amountBlockState = amountBlockState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content( + stringReference( + BigDecimal.ZERO.format { + crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true) + }, + ), + ), + ), + continueButtonConfig = continueButtonConfig.copy(enabled = false), + offersBlockState = OnrampOffersBlockUM.Empty, + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencySymbol = amountBlockState.currencyUM.unit, + currencyCode = amountBlockState.currencyUM.code, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), + onrampProviderState = OnrampV2ProvidersUM.Empty, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt new file mode 100644 index 0000000000..4bf1a7285a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt @@ -0,0 +1,35 @@ +package com.tangem.features.onramp.mainv2.entity.factory + +import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState +import kotlinx.collections.immutable.toPersistentList + +internal class OnrampAmountButtonUMStateFactory { + + private val defaultPreselectedAmount = listOf(50, 100, 200, 300, 500) + + fun createOnrampAmountActionButton( + currencyCode: String, + currencySymbol: String, + onAmountValueChanged: (String) -> Unit, + ): OnrampV2AmountButtonUMState { + return when (currencyCode) { + USD_CODE, EUR_CODE -> { + val buttons = defaultPreselectedAmount.map { value -> + OnrampAmountButtonUM( + value = value, + currency = currencySymbol, + onClick = { onAmountValueChanged(value.toString()) }, + ) + }.toPersistentList() + OnrampV2AmountButtonUMState.Loaded(buttons) + } + else -> OnrampV2AmountButtonUMState.None + } + } + + companion object { + private const val USD_CODE = "USD" + private const val EUR_CODE = "EUR" + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt new file mode 100644 index 0000000000..3c7a477fd1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt @@ -0,0 +1,222 @@ +package com.tangem.features.onramp.mainv2.entity.factory + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.combinedReference +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.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.AmountType +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampIntents +import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class OnrampV2AmountStateFactory( + private val currentStateProvider: Provider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val onrampIntents: OnrampIntents, + private val cryptoCurrency: CryptoCurrency, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, +) { + + private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy( + mode = LazyThreadSafetyMode.NONE, + ) { + OnrampV2AmountFieldChangeConverter( + currentStateProvider = currentStateProvider, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + onrampIntents = onrampIntents, + cryptoCurrency = cryptoCurrency, + ) + } + + fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM { + return onrampAmountFieldChangeConverter.convert(value) + } + + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + + return currentState.copy( + amountBlockState = amountState.copy( + currencyUM = amountState.currencyUM.copy( + unit = currency.unit, + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + ), + amountFieldModel = amountState.amountFieldModel.copy( + isError = false, + fiatAmount = amountState.amountFieldModel.fiatAmount.copy( + currencySymbol = currency.unit, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ), + ), + ), + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencyCode = currency.code, + currencySymbol = currency.unit, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), + ) + } + + fun getAmountSecondaryLoadingState(): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + + return currentState.copy( + amountBlockState = amountState.copy( + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading, + ), + offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false), + continueButtonConfig = currentState.continueButtonConfig.copy(enabled = false), + errorNotification = null, + onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampProviderState = OnrampV2ProvidersUM.Loading, + ) + } + + fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState + + return currentState.copy( + amountBlockState = amountState.copy( + amountFieldModel = amountState.amountFieldModel.copy(isError = false), + secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, + ), + continueButtonConfig = currentState.continueButtonConfig.copy( + enabled = quote is OnrampQuote.Data, + onClick = onrampIntents::onContinueClick, + ), + errorNotification = null, + ) + } + + fun getUpdatedProviderState(selectedQuote: OnrampQuote): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + analyticsEventHandler.send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = selectedQuote.provider.info.name, + tokenSymbol = cryptoCurrency.symbol, + paymentMethod = selectedQuote.paymentMethod.name, + ), + ) + return currentState.copy( + onrampProviderState = selectedQuote.toProviderBlockState(), + ) + } + + fun getAmountSecondaryResetState(): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + + if (amountState.secondaryFieldModel is OnrampNewAmountSecondaryFieldUM.Content) return currentState + + return currentState.copy( + amountBlockState = amountState.copy( + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content( + amount = stringReference( + BigDecimal.ZERO.format { + crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true) + }, + ), + ), + ), + onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + errorNotification = null, + ) + } + + fun getShowProvidersState(): OnrampV2MainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampV2MainComponentUM.Content) return currentState + + return when (currentState.offersBlockState) { + is OnrampOffersBlockUM.Content -> { + currentState.copy( + offersBlockState = currentState.offersBlockState.copy(isBlockVisible = true), + ) + } + OnrampOffersBlockUM.Empty, + is OnrampOffersBlockUM.Loading, + -> currentState + } + } + + private fun OnrampQuote.toProviderBlockState(): OnrampV2ProvidersUM { + return OnrampV2ProvidersUM.Content( + paymentMethod = paymentMethod, + providerId = provider.id, + ) + } + + private fun OnrampQuote.toSecondaryFieldUiModel( + amountState: OnrampNewAmountBlockUM, + ): OnrampNewAmountSecondaryFieldUM? { + return when (this) { + is OnrampQuote.Error -> null + is OnrampQuote.Data -> { + val amount = toAmount.value.format { + crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true) + } + val contentAmount = combinedReference(stringReference("\u007E"), stringReference(amount)) + OnrampNewAmountSecondaryFieldUM.Content(contentAmount) + } + is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) + } + } + + private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( + amountState: OnrampNewAmountBlockUM, + ): OnrampNewAmountSecondaryFieldUM.Error { + val amount = error.requiredAmount.format { + fiat( + fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, + fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, + ) + } + + val errorTextRes = when (error) { + is OnrampError.AmountError.TooBigError -> { + analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + R.string.onramp_max_amount_restriction + } + is OnrampError.AmountError.TooSmallError -> { + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) + R.string.onramp_min_amount_restriction + } + } + + return OnrampNewAmountSecondaryFieldUM.Error( + resourceReference( + errorTextRes, + wrappedList(amount), + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt new file mode 100644 index 0000000000..8e37235b44 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt @@ -0,0 +1,205 @@ +package com.tangem.features.onramp.mainv2.entity.factory + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.convertToAmount +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampIntents +import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class OnrampV2StateFactory( + private val currentStateProvider: Provider, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, + private val cryptoCurrency: CryptoCurrency, + private val onrampIntents: OnrampIntents, +) { + + fun getInitialState( + currency: String, + onClose: () -> Unit, + openSettings: () -> Unit, + ): OnrampV2MainComponentUM.InitialLoading { + return OnrampV2MainComponentUM.InitialLoading( + errorNotification = null, + topBarConfig = OnrampV2MainTopBarUM( + title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), + startButtonUM = TopAppBarButtonUM.Close( + onCloseClick = onClose, + enabled = true, + ), + endButtonUM = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_more_vertical_24, + onClicked = openSettings, + enabled = false, + ), + ), + continueButtonConfig = ContinueButtonUM( + text = resourceReference(R.string.common_continue), + onClick = {}, + enabled = false, + ), + ) + } + + fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content { + val state = currentStateProvider() + + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } + + val initialAmountBlockState = getInitialAmountBlockState(currency) + + return OnrampV2MainComponentUM.Content( + topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), + continueButtonConfig = ContinueButtonUM( + text = resourceReference(R.string.common_continue), + onClick = onrampIntents::onContinueClick, + enabled = false, + ), + amountBlockState = initialAmountBlockState, + offersBlockState = OnrampOffersBlockUM.Empty, + errorNotification = null, + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencyCode = currency.code, + currencySymbol = currency.unit, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), + onrampProviderState = OnrampV2ProvidersUM.Empty, + ) + } + + fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM { + return when (onrampError) { + OnrampError.PairsNotFound -> getNoPairsErrorState() + is OnrampError.DataError -> getErrorState( + errorCode = onrampError.code, + onRefresh = onrampIntents::onRefresh, + ) + is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh) + is OnrampError.AmountError.TooBigError, + is OnrampError.AmountError.TooSmallError, + OnrampError.RedirectError.VerificationFailed, + OnrampError.RedirectError.WrongRequestId, + -> currentStateProvider() // ignore error state + } + } + + private fun getNoPairsErrorState(): OnrampV2MainComponentUM { + val state = currentStateProvider() + val contentState = state as? OnrampV2MainComponentUM.Content ?: return state + + return contentState.copy( + continueButtonConfig = contentState.continueButtonConfig.copy(enabled = false), + amountBlockState = contentState.amountBlockState.copy( + amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Error( + error = resourceReference(R.string.onramp_no_available_providers), + ), + ), + onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, + ) + } + + fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM { + val state = currentStateProvider() + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } + + return when (state) { + is OnrampV2MainComponentUM.Content -> state.copy( + topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), + continueButtonConfig = state.continueButtonConfig.copy(enabled = false), + amountBlockState = state.amountBlockState.copy( + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content( + stringReference( + BigDecimal.ZERO.format { + crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true) + }, + ), + ), + ), + offersBlockState = OnrampOffersBlockUM.Empty, + errorNotification = NotificationUM.Warning.OnrampErrorNotification( + errorCode = errorCode, + onRefresh = onRefresh, + ), + onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampProviderState = OnrampV2ProvidersUM.Empty, + ) + is OnrampV2MainComponentUM.InitialLoading -> state.copy( + errorNotification = NotificationUM.Warning.OnrampErrorNotification( + errorCode = errorCode, + onRefresh = onRefresh, + ), + ) + } + } + + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM { + return OnrampNewAmountBlockUM( + currencyUM = OnrampNewCurrencyUM( + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + onClick = onrampIntents::openCurrenciesList, + unit = currency.unit, + ), + amountFieldModel = AmountFieldModel( + value = "", + fiatValue = "", + onValueChange = onrampIntents::onAmountValueChanged, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + isFiatValue = true, + cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), + fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), + isError = false, + isWarning = false, + error = TextReference.EMPTY, + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + ), + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content( + stringReference( + BigDecimal.ZERO.format { + crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true) + }, + ), + ), + ) + } + + private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( + currencySymbol = currency.unit, + value = this, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt new file mode 100644 index 0000000000..8d6615a675 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt @@ -0,0 +1,359 @@ +package com.tangem.features.onramp.mainv2.model + +import androidx.compose.runtime.mutableStateOf +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.fields.InputManager +import com.tangem.domain.onramp.* +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.onramp.main.entity.OnrampIntents +import com.tangem.features.onramp.main.entity.OnrampLastUpdate +import com.tangem.features.onramp.mainv2.OnrampV2MainComponent +import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory +import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory +import com.tangem.features.onramp.utils.sendOnrampErrorEvent +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class OnrampV2MainComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val router: Router, + private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, + private val getOnrampCountryUseCase: GetOnrampCountryUseCase, + private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, + private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, + private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, + private val fetchPairsUseCase: OnrampFetchPairsUseCase, + private val amountInputManager: InputManager, + paramsContainer: ParamsContainer, + getWalletsUseCase: GetWalletsUseCase, +) : Model(), OnrampIntents { + + val params = paramsContainer.require() + + private var loadQuotesJob: Job? = null + + private val lastUpdateState = mutableStateOf(null) + + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountButtonUMStateFactory() + } + + private val stateFactory = OnrampV2StateFactory( + currentStateProvider = Provider { _state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + + private val amountStateFactory = OnrampV2AmountStateFactory( + currentStateProvider = Provider { _state.value }, + analyticsEventHandler = analyticsEventHandler, + onrampIntents = this, + cryptoCurrency = params.cryptoCurrency, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + + private val _state: MutableStateFlow = MutableStateFlow( + value = stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = ::onCloseClick, + openSettings = ::openSettings, + ), + ) + val state: StateFlow get() = _state.asStateFlow() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + + init { + modelScope.launch { + clearOnrampCacheUseCase() + } + + sendScreenOpenAnalytics() + checkResidenceCountry() + subscribeToAmountChanges() + subscribeToCountryAndCurrencyUpdates() + subscribeToQuotesUpdate() + } + + override fun onDestroy() { + modelScope.launch { clearOnrampCacheUseCase.invoke() } + loadQuotesJob?.cancel() + super.onDestroy() + } + + override fun onAmountValueChanged(value: String) { + _state.update { amountStateFactory.getOnAmountValueChange(value) } + modelScope.launch { amountInputManager.update(value) } + } + + override fun openSettings() { + params.openSettings + } + + override fun openCurrenciesList() { + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened) + bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) + } + + override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + // TODO in [REDACTED_TASK_KEY] to be continued + } + + override fun openProviders() { + // TODO in [REDACTED_TASK_KEY] to be continued + } + + override fun onRefresh() { + _state.update { + stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = router::pop, + openSettings = ::openSettings, + ) + } + loadQuotesJob?.cancel() + modelScope.launch { + clearOnrampCacheUseCase.invoke() + checkResidenceCountry() + } + } + + override fun onLinkClick(link: String) { + // TODO in [REDACTED_TASK_KEY] will be deleted + } + + override fun onContinueClick() { + val currentState = _state.value + if (currentState is OnrampV2MainComponentUM.Content) { + _state.update { amountStateFactory.getShowProvidersState() } + } + } + + private fun checkResidenceCountry() { + modelScope.launch { + checkOnrampAvailabilityUseCase(userWallet) + .onRight(::handleOnrampAvailability) + .onLeft(::handleOnrampError) + } + } + + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> Unit + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country)) + } + } + + private fun onCloseClick() { + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + router.pop() + } + + private fun subscribeToAmountChanges() = modelScope.launch { + amountInputManager.query + .filter(String::isNotEmpty) + .collectLatest { _ -> + _state.update { amountStateFactory.getAmountSecondaryLoadingState() } + loadQuotes() + } + } + + private fun subscribeToCountryAndCurrencyUpdates() { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + maybeCountry.fold( + ifLeft = ::handleOnrampError, + ifRight = { country -> + if (country == null) return@onEach + _state.update { + when (it) { + is OnrampV2MainComponentUM.Content -> { + amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + } + is OnrampV2MainComponentUM.InitialLoading -> { + stateFactory.getReadyState(country.defaultCurrency) + } + } + } + updatePairsAndQuotes() + }, + ) + } + .launchIn(modelScope) + } + + private fun subscribeToQuotesUpdate() { + getOnrampQuotesUseCase.invoke() + .conflate() + .onEach { maybeQuotes -> + maybeQuotes.fold( + ifLeft = ::handleOnrampError, + ifRight = ::handleQuoteResult, + ) + } + .launchIn(modelScope) + } + + private fun handleQuoteResult(quotes: List) { + sendOnrampQuotesErrorAnalytic(quotes) + + val quote = selectOrUpdateQuote(quotes) + + if (quote == null) { + _state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + lastUpdateState.value = null + return + } + _state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } + } + + private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { + val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } + + // Check if amount, country or currency has changed + val newQuote = if (checkLastInputState(quoteToCheck)) { + quoteToCheck + } else { + val state = state.value as? OnrampV2MainComponentUM.Content + val providerState = state?.onrampProviderState as? OnrampV2ProvidersUM.Content + + // Get current selected quote to update + val lastSelectedQuote = quotes.firstOrNull { + it.provider.id == providerState?.providerId && + it.paymentMethod.id == providerState.paymentMethod.id + } + + if (lastSelectedQuote is OnrampQuote.Error) { + quoteToCheck + } else { + lastSelectedQuote + } + } + newQuote?.let { updateProvider(newQuote) } + + return newQuote + } + + private fun onRetryQuotes() { + _state.update { + (it as? OnrampV2MainComponentUM.Content)?.copy( + errorNotification = null, + onrampProviderState = OnrampV2ProvidersUM.Loading, + offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false), + amountBlockState = it.amountBlockState.copy( + secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading, + ), + ) ?: it + } + loadQuotes() + } + + private suspend fun updatePairsAndQuotes() { + val state = state.value as? OnrampV2MainComponentUM.Content + + if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) { + _state.update { amountStateFactory.getAmountSecondaryLoadingState() } + } + fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( + ifLeft = ::handleOnrampError, + ifRight = { + _state.update { + if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) { + return@fold + } else { + amountStateFactory.getAmountSecondaryResetState() + } + } + }, + ) + loadQuotes() + } + + private fun loadQuotes() { + loadQuotesJob?.cancel() + loadQuotesJob = modelScope.launch { + runCatching { + val content = state.value as? OnrampV2MainComponentUM.Content ?: return@runCatching + if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching + fetchQuotesUseCase.invoke( + userWallet = userWallet, + amount = content.amountBlockState.amountFieldModel.fiatAmount, + cryptoCurrency = params.cryptoCurrency, + ).onLeft(::handleOnrampError) + } + } + } + + private fun handleOnrampError(onrampError: OnrampError) { + Timber.e(onrampError.toString()) + _state.update { stateFactory.getOnrampErrorState(onrampError) } + } + + private fun updateProvider(quote: OnrampQuote) { + lastUpdateState.value = OnrampLastUpdate( + quote.fromAmount, + quote.countryCode, + ) + + _state.update { + amountStateFactory.getUpdatedProviderState(selectedQuote = quote) + } + } + + private fun sendOnrampQuotesErrorAnalytic(quotes: List) { + quotes.forEach { errorState -> + when (errorState) { + is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent( + error = errorState.error, + tokenSymbol = params.cryptoCurrency.symbol, + providerName = errorState.provider.info.name, + paymentMethod = errorState.paymentMethod.name, + ) + is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent( + error = errorState.error, + tokenSymbol = params.cryptoCurrency.symbol, + providerName = errorState.provider.info.name, + paymentMethod = errorState.paymentMethod.name, + ) + else -> Unit + } + } + } + + private fun checkLastInputState(quote: OnrampQuote?): Boolean { + return lastUpdateState.value?.lastAmount != quote?.fromAmount || + lastUpdateState.value?.lastCountryString != quote?.countryCode + } + + private fun sendScreenOpenAnalytics() { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ScreenOpened( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt new file mode 100644 index 0000000000..5668fac482 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt @@ -0,0 +1,130 @@ +package com.tangem.features.onramp.mainv2.ui + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState +import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM + +@Composable +internal fun OnrampFooterContent( + state: OnrampV2MainComponentUM.Content, + boxScope: BoxScope, + modifier: Modifier = Modifier, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + boxScope.apply { + AnimatedVisibility( + modifier = Modifier + .imePadding() + .align(Alignment.BottomCenter), + visible = state.offersBlockState.isBlockVisible.not(), + enter = slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(durationMillis = 300), + ), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(durationMillis = 300), + ), + label = "Footer block animation", + ) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResourceSafe(id = R.string.common_continue), + onClick = { + state.continueButtonConfig.onClick() + keyboardController?.hide() + }, + enabled = state.continueButtonConfig.enabled, + ) + SpacerH(16.dp) + OnrampAmountButtons(state = state.onrampAmountButtonUMState) + } + } + } +} + +@Composable +private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { + val keyboard by keyboardAsState() + + AnimatedVisibility( + visible = state is OnrampV2AmountButtonUMState.Loaded, + enter = fadeIn(), + exit = fadeOut(), + ) { + when (state) { + is OnrampV2AmountButtonUMState.Loaded -> { + if (keyboard is Keyboard.Opened) { + LazyRow( + modifier = Modifier.background(color = TangemTheme.colors.button.secondary), + contentPadding = PaddingValues( + vertical = 10.dp, + horizontal = 8.dp, + ), + state = rememberLazyListState(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + items = state.amountButtons, + key = OnrampAmountButtonUM::value, + ) { + AmountButton(button = it) + } + } + } + } + OnrampV2AmountButtonUMState.None -> Unit + } + } +} + +@Composable +private fun AmountButton(button: OnrampAmountButtonUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .sizeIn(minHeight = 24.dp, minWidth = 62.dp) + .background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(16.dp), + ) + .clickable(onClick = button.onClick) + .padding(vertical = 4.dp, horizontal = 20.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "${button.value}${button.currency}", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt new file mode 100644 index 0000000000..cf0d2ca599 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt @@ -0,0 +1,141 @@ +package com.tangem.features.onramp.mainv2.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM + +@Composable +internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier.systemBarsPadding(), + topBar = { + TangemTopAppBar( + startButton = state.topBarConfig.startButtonUM, + endButton = state.topBarConfig.endButtonUM, + title = state.topBarConfig.title.resolveReference(), + ) + }, + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + OnrampNewMainComponentContent( + state = state, + modifier = Modifier.padding(scaffoldPaddings), + ) + } +} + +@Composable +internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (state) { + is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state) + is OnrampV2MainComponentUM.Content -> Content(state = state) + } + } + + if (state is OnrampV2MainComponentUM.Content) { + OnrampFooterContent( + state = state, + boxScope = this, + ) + } + } +} + +@Composable +private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + OnrampAmountContentLoading() + if (state.errorNotification != null) Notification(config = state.errorNotification.config) + } +} + +@Composable +private fun OnrampAmountContentLoading() { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action) + .padding(vertical = TangemTheme.dimens.spacing28), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .size(width = 76.dp, height = 20.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12) + .size(width = 136.dp, height = 44.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(width = 52.dp, height = 16.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing20) + .size(width = 84.dp, height = 28.dp), + radius = TangemTheme.dimens.radius14, + ) + } +} + +@Composable +private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .navigationBarsPadding() + .padding( + bottom = 76.dp, + start = 16.dp, + end = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + OnrampV2AmountContent(state = state) + + OnrampOffersContent(state = state.offersBlockState) + + if (state.errorNotification != null) Notification(config = state.errorNotification.config) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt new file mode 100644 index 0000000000..6b35933d9e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt @@ -0,0 +1,32 @@ +package com.tangem.features.onramp.mainv2.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM + +@Composable +internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { + AnimatedVisibility( + visible = state.isBlockVisible, + enter = slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(durationMillis = 300), + ), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(durationMillis = 300), + ), + label = "Offers block animation", + ) { + Text( + text = "Some offers", + style = TangemTheme.typography.head, + ) + // TODO in [REDACTED_TASK_KEY] to be continued + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt new file mode 100644 index 0000000000..284e2e04cc --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt @@ -0,0 +1,199 @@ +package com.tangem.features.onramp.mainv2.ui + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags +import com.tangem.core.ui.utils.rememberDecimalFormat +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.mainv2.entity.OnrampNewAmountSecondaryFieldUM +import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM +import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM + +@Composable +internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { + val padding = remember(state.offersBlockState.isBlockVisible) { + if (state.offersBlockState.isBlockVisible) { + 22.dp + } else { + 46.dp + } + } + + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + ) + .padding(vertical = padding) + .animateContentSize(animationSpec = tween(durationMillis = 300)), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + OnrampHeaderTitle() + + SpacerH(12.dp) + + OnrampAmountField( + amountField = state.amountBlockState.amountFieldModel, + currencyCode = state.amountBlockState.currencyUM.code, + ) + + SpacerH(8.dp) + + OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + + SpacerH(20.dp) + + OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) + } +} + +@Composable +private fun OnrampHeaderTitle() { + Text( + text = stringResourceSafe(R.string.onramp_you_will_pay_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { + val decimalFormat = rememberDecimalFormat() + val requester = remember { FocusRequester() } + AmountTextField( + value = amountField.fiatValue, + decimals = amountField.fiatAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = amountField.fiatAmount.decimals, + symbol = currencyCode, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + symbolColor = TangemTheme.colors.text.disabled, + ), + onValueChange = amountField.onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, + textStyle = TangemTheme.typography.head.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isEnabled = !amountField.isError, + isAutoResize = true, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), + ) + + LaunchedEffect(key1 = Unit) { + requester.requestFocus() + } +} + +@Composable +private fun OnrampAmountSecondary(state: OnrampNewAmountSecondaryFieldUM) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT), + contentAlignment = Alignment.Center, + ) { + when (state) { + is OnrampNewAmountSecondaryFieldUM.Content -> Text( + text = state.amount.resolveReference(), + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + is OnrampNewAmountSecondaryFieldUM.Error -> Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) + is OnrampNewAmountSecondaryFieldUM.Loading -> TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(TangemTheme.dimens.size62), + ) + } + } +} + +@Composable +private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.button.secondary) + .clickable(onClick = currencyUM.onClick) + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + AsyncImage( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), + model = currencyUM.iconUrl, + contentDescription = null, + ) + Text( + text = currencyUM.code, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), + textAlign = TextAlign.Center, + ) + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt deleted file mode 100644 index 5eab7b94c5..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/newmain/OnrampNewMainFeatureToggle.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onramp.newmain - -internal interface OnrampNewMainFeatureToggle { - val isOnrampNewMainEnabled: Boolean -} \ No newline at end of file From 02f927fd48383684915d3adddb4992d10b0e34eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Sep 2025 17:33:18 +0300 Subject: [PATCH 41/48] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 4 + .../com/tangem/common/utils/NetworkUtils.kt | 105 ++++++++++ .../tangem/screens/MainScreenPageObject.kt | 4 + .../WalletConnectBottonSheetPageObject.kt | 108 +++++++++++ ...lletConnectDetailsBottonSheetPageObject.kt | 107 +++++++++++ .../tangem/screens/WalletConnectPageObject.kt | 59 ++++++ .../kotlin/com/tangem/steps/BaseScenarios.kt | 38 ++++ .../com/tangem/steps/DeepLinksScenarios.kt | 15 ++ .../tangem/steps/WalletConnectScenarios.kt | 139 ++++++++++++++ .../com/tangem/tests/WalletConnectTest.kt | 181 ++++++++++++++++++ .../modal/TangemModalBottomSheetTitle.kt | 13 +- .../test/WalletConnectBottomSheetTestTags.kt | 22 +++ ...WalletConnectDetailsBottomSheetTestTags.kt | 16 ++ .../ui/test/WalletConnectScreenTestTags.kt | 10 + .../connections/ui/InternalComponents.kt | 30 ++- .../connections/ui/WcAppInfoBS.kt | 39 +++- .../connections/ui/WcConnectedAppInfoBS.kt | 18 +- .../connections/ui/WcConnectionsContent.kt | 19 +- 18 files changed, 898 insertions(+), 29 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletConnectDetailsBottonSheetPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectDetailsBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 7fd98efe81..7ab38e88e2 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -126,6 +126,10 @@ abstract class BaseTestCase : TestCase( with(featureTogglesManager as MutableFeatureTogglesManager) { changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true) changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true) + changeToggle("SEND_VIA_SWAP_ENABLED", true) + changeToggle("SWAP_REDESIGN_ENABLED", true) + changeToggle("SEND_REDESIGN_ENABLED", true) + changeToggle("NEW_ONRAMP_MAIN_ENABLED", true) } } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt new file mode 100644 index 0000000000..da1573d026 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt @@ -0,0 +1,105 @@ +package com.tangem.common.utils + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import timber.log.Timber +import java.util.concurrent.TimeUnit + +/** + * + */ +fun getWcUri( + network: String = "ethereum", + baseUrl: String = "[REDACTED_ENV_URL]" +): String? { + Timber.i("Getting WC URI for network: $network") + + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения + .readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа + .writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи + .callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса + .build() + + val request = Request.Builder() + .url("$baseUrl/wc_uri?network=$network") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + Timber.i("Response code: ${response.code}") + + if (response.isSuccessful) { + val body = response.body?.string() ?: "" + Timber.i("Response body: $body") + + val jsonObject = JSONObject(body) + + if (jsonObject.getBoolean("success")) { + val wcUri = jsonObject.getString("wcUri") + Timber.i("Got WC URI successfully: $wcUri") + + wcUri + } else { + Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}") + null + } + } else { + val errorBody = response.body?.string() ?: "No error body" + Timber.e("Request failed: ${response.code}, body: $errorBody") + null + } + } + } catch (e: Exception) { + Timber.e(e, "Error getting WC URI") + null + } +} + +fun checkServiceHealth( + baseUrl: String = "[REDACTED_ENV_URL]" +): String? { + Timber.i("Checking service health") + + val client = OkHttpClient() + val request = Request.Builder() + .url("$baseUrl/health") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + Timber.i("Response code: ${response.code}") + + if (response.isSuccessful) { + val body = response.body?.string() ?: "" + Timber.i("Response body: $body") + + if (body.isEmpty()) { + Timber.e("Response body is empty") + return null + } + + val jsonObject = JSONObject(body) + val status = jsonObject.optString("status", "") + + if (status.isNotEmpty()) { + Timber.i("Got status successfully: $status") + status + } else { + Timber.e("Status field is missing or empty") + null + } + } else { + val errorBody = response.body?.string() ?: "No error body" + Timber.e("Request failed: ${response.code}, body: $errorBody") + null + } + } + } catch (e: Exception) { + Timber.e(e, "Error checking health") + null + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index ce3a3181b3..56742890d9 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -35,6 +35,10 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } ) + val screenContainer: KNode = child { + hasTestTag(MainScreenTestTags.SCREEN_CONTAINER) + } + val synchronizeAddressesButton: KNode = child { hasText(getResourceString(R.string.common_generate_addresses)) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt new file mode 100644 index 0000000000..c111e92406 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt @@ -0,0 +1,108 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.walletconnect.impl.R as WalletConnectImplR + +class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect)) + hasTestTag(WalletConnectBottomSheetTestTags.TITLE) + useUnmergedTree = true + } + + val appIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON) + useUnmergedTree = true + } + + val appName: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME) + useUnmergedTree = true + } + + val approveIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON) + useUnmergedTree = true + } + + val appUrl: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_URL) + useUnmergedTree = true + } + + val connectionRequestIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_ICON) + useUnmergedTree = true + } + + val connectionRequestText: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_TEXT) + useUnmergedTree = true + } + + val connectionRequestChevron: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_CHEVRON) + useUnmergedTree = true + } + + val walletIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.WALLET_ICON) + useUnmergedTree = true + } + + val walletNameTitle: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE) + useUnmergedTree = true + } + + val walletName: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME) + useUnmergedTree = true + } + + val networksIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICON) + useUnmergedTree = true + } + + val networksTitle: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_TITLE) + useUnmergedTree = true + } + + val networksIcons: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICONS) + useUnmergedTree = true + } + + val networksSelectorIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON) + useUnmergedTree = true + } + + val cancelButton: KNode = child { + hasText(getResourceString(R.string.common_cancel)) + hasTestTag(BaseButtonTestTags.TEXT) + useUnmergedTree = true + } + + val connectButton: KNode = child { + hasText(getResourceString(R.string.wc_common_connect)) + hasTestTag(BaseButtonTestTags.TEXT) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onWalletConnectBottomSheet(function: WalletConnectBottomSheetPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectDetailsBottonSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectDetailsBottonSheetPageObject.kt new file mode 100644 index 0000000000..e4eb48fde7 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectDetailsBottonSheetPageObject.kt @@ -0,0 +1,107 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags +import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.walletconnect.impl.R as WalletConnectImplR + +class WalletConnectDetailsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.TITLE) + hasText(getResourceString(WalletConnectImplR.string.wc_connected_app_title)) + useUnmergedTree = true + } + + val date: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.DATE) + useUnmergedTree = true + } + + val closeButton: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val networksBlockTitle: KNode = child { + hasText(getResourceString(WalletConnectImplR.string.wc_connected_networks)) + useUnmergedTree = true + } + + val appIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON) + useUnmergedTree = true + } + + val approveIcon: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON) + useUnmergedTree = true + } + + val appName: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME) + useUnmergedTree = true + } + + val appUrl: KNode = child { + hasTestTag(WalletConnectBottomSheetTestTags.APP_URL) + useUnmergedTree = true + } + + val walletIcon: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON) + useUnmergedTree = true + } + + val walletTitle: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE) + useUnmergedTree = true + } + + val walletName: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME) + useUnmergedTree = true + } + + val connectedNetworksTitle: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORKS_TITLE) + useUnmergedTree = true + } + + val connectedNetworkItem: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ITEM) + useUnmergedTree = true + } + + val connectedNetworkIcon: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ICON) + useUnmergedTree = true + } + + val connectedNetworkName: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_NAME) + useUnmergedTree = true + } + + val connectedNetworkSymbol: KNode = child { + hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_SYMBOL) + useUnmergedTree = true + } + + val disconnectButton: KNode = child { + hasText(getResourceString(WalletConnectImplR.string.common_disconnect)) + hasTestTag(BaseButtonTestTags.TEXT) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onWalletConnectDetailsBottomSheet(function: WalletConnectDetailsBottomSheetPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt new file mode 100644 index 0000000000..b9da100325 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt @@ -0,0 +1,59 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.WalletConnectScreenTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(R.string.wc_connections)) + useUnmergedTree = true + } + + val moreButton: KNode = child { + hasTestTag(WalletConnectScreenTestTags.MORE_BUTTON) + useUnmergedTree = true + } + + val walletName: KNode = child { + hasTestTag(WalletConnectScreenTestTags.WALLET_NAME) + useUnmergedTree = true + } + + val appIcon: KNode = child { + hasTestTag(WalletConnectScreenTestTags.APP_ICON) + useUnmergedTree = true + } + + val appName: KNode = child { + hasTestTag(WalletConnectScreenTestTags.APP_NAME) + useUnmergedTree = true + } + + val approveIcon: KNode = child { + hasTestTag(WalletConnectScreenTestTags.APPROVE_ICON) + useUnmergedTree = true + } + + val appUrl: KNode = child { + hasTestTag(WalletConnectScreenTestTags.APP_URL) + useUnmergedTree = true + } + + val newConnectionButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.wc_new_connection)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt new file mode 100644 index 0000000000..54f4f8a915 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt @@ -0,0 +1,38 @@ +package com.tangem.steps + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.screens.onDisclaimerScreen +import com.tangem.screens.onMainScreen +import com.tangem.screens.onMarketsTooltipScreen +import com.tangem.screens.onStoriesScreen +import com.tangem.tap.domain.sdk.mocks.MockProvider +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.openMainScreen(productType: ProductType? = null) { + if (productType != null) { + MockProvider.setMocks(productType) + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Click on 'Accept' button") { + onStoriesScreen { scanButton.clickWithAssertion() } + } + step("Assert main screen is displayed") { + onMainScreen { screenContainer.assertIsDisplayed() } + } + step("Assert main screen is displayed") { + onMarketsTooltipScreen { contentContainer.clickWithAssertion() } + } +} + +fun BaseTestCase.synchronizeAddresses(balance: String) { + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt b/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt new file mode 100644 index 0000000000..832a1b9fe8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt @@ -0,0 +1,15 @@ +package com.tangem.steps + +import android.content.Intent +import android.net.Uri +import androidx.test.core.app.ApplicationProvider + +fun openAppByDeepLink(deepLinkUri: String?) { + val deeplinkScheme = "tangem://wc?uri=" + val context = ApplicationProvider.getApplicationContext() + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + context.startActivity(intent) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt b/app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt new file mode 100644 index 0000000000..77134c2ea3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt @@ -0,0 +1,139 @@ +package com.tangem.steps + +import com.tangem.common.BaseTestCase +import com.tangem.screens.onWalletConnectBottomSheet +import com.tangem.screens.onWalletConnectDetailsBottomSheet +import com.tangem.screens.onWalletConnectScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.checkWalletConnectBottomSheet() { + step("Assert 'Wallet Connect' bottom sheet title is displayed") { + onWalletConnectBottomSheet { title.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet app icon is displayed") { + onWalletConnectBottomSheet { appIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet app name is displayed") { + onWalletConnectBottomSheet { appName.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet approve icon is displayed") { + onWalletConnectBottomSheet { approveIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet app URL is displayed") { + onWalletConnectBottomSheet { appUrl.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet connection request icon is displayed") { + onWalletConnectBottomSheet { connectionRequestIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet connection request text is displayed") { + onWalletConnectBottomSheet { connectionRequestText.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet connection request chevron is displayed") { + onWalletConnectBottomSheet { connectionRequestChevron.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet wallet icon is displayed") { + onWalletConnectBottomSheet { walletIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet wallet title is displayed") { + onWalletConnectBottomSheet { walletNameTitle.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet wallet name is displayed") { + onWalletConnectBottomSheet { walletName.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet networks icon is displayed") { + onWalletConnectBottomSheet { networksIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet networks title is displayed") { + onWalletConnectBottomSheet { networksTitle.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet right networks icons is displayed") { + onWalletConnectBottomSheet { networksIcons.assertIsDisplayed() } + } + step("'Wallet Connect' bottom sheet networks selector icon is displayed") { + onWalletConnectBottomSheet { networksSelectorIcon.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet 'Cancel' button is displayed") { + onWalletConnectBottomSheet { cancelButton.assertIsDisplayed() } + } + step("Assert 'Wallet Connect' bottom sheet 'Connect' button is displayed") { + onWalletConnectBottomSheet { connectButton.assertIsDisplayed() } + } +} + +fun BaseTestCase.checkWalletConnectScreen() { + step("Assert 'Wallet Connect' title is displayed") { + onWalletConnectScreen { title.assertIsDisplayed() } + } + step("Assert 'More' button is displayed") { + onWalletConnectScreen { moreButton.assertIsDisplayed() } + } + step("Assert wallet name is displayed") { + onWalletConnectScreen { walletName.assertIsDisplayed() } + } + step("Assert app icon is displayed") { + onWalletConnectScreen { appIcon.assertIsDisplayed() } + } + step("Assert app name is displayed") { + onWalletConnectScreen { appName.assertIsDisplayed() } + } + step("Assert approve icon is displayed") { + onWalletConnectScreen { approveIcon.assertIsDisplayed() } + } + step("Assert app URL is displayed") { + onWalletConnectScreen { appUrl.assertIsDisplayed() } + } + step("Assert 'New Connection' button is displayed") { + onWalletConnectScreen { newConnectionButton.assertIsDisplayed() } + } +} + +fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { + step("Assert connection details title is displayed") { + onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() } + } + step("Assert date is displayed") { + onWalletConnectDetailsBottomSheet { date.assertIsDisplayed() } + } + step("Assert 'Close' button is displayed") { + onWalletConnectDetailsBottomSheet { closeButton.assertIsDisplayed() } + } + step("Assert app icon is displayed") { + onWalletConnectDetailsBottomSheet { appIcon.assertIsDisplayed() } + } + step("Assert app name is displayed") { + onWalletConnectDetailsBottomSheet { appName.assertIsDisplayed() } + } + step("Assert approve icon is displayed") { + onWalletConnectDetailsBottomSheet { approveIcon.assertIsDisplayed() } + } + step("Assert app URL is displayed") { + onWalletConnectDetailsBottomSheet { appUrl.assertIsDisplayed() } + } + step("Assert wallet icon is displayed") { + onWalletConnectDetailsBottomSheet { walletIcon.assertIsDisplayed() } + } + step("Assert wallet title is displayed") { + onWalletConnectDetailsBottomSheet { walletTitle.assertIsDisplayed() } + } + step("Assert wallet name is displayed") { + onWalletConnectDetailsBottomSheet { walletName.assertIsDisplayed() } + } + step("Assert 'Connected networks' title is displayed") { + onWalletConnectDetailsBottomSheet { connectedNetworksTitle.assertIsDisplayed() } + } + step("Assert connected network item is displayed") { + onWalletConnectDetailsBottomSheet { connectedNetworkItem.assertIsDisplayed() } + } + step("Assert connected network icon is displayed") { + onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() } + } + step("Assert connected dApp name: '$dAppName'") { + onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) } + } + step("Assert connected network symbol is displayed") { + onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() } + } + step("Assert 'Disconnect button' is displayed") { + onWalletConnectDetailsBottomSheet { disconnectButton.assertIsDisplayed() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt new file mode 100644 index 0000000000..ff9f1239bd --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt @@ -0,0 +1,181 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeUp +import com.tangem.common.utils.getWcUri +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import com.tangem.steps.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore +import org.junit.Test + +@HiltAndroidTest +class WalletConnectTest : BaseTestCase() { + + @AllureId("3833") + @DisplayName("WC (React App): open session from deeplink on main screen") + @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") + @Test + fun openWalletConnectSessionOnMainScreen() { + val balance = TOTAL_BALANCE + val dAppName = "React App" + val deepLinkUri = getWcUri() + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Create WC session buy deeplink") { + openAppByDeepLink(deepLinkUri) + } + step("Check 'Wallet Connect' bottom sheet") { + checkWalletConnectBottomSheet() + } + step("Click on 'Connect' button") { + onWalletConnectBottomSheet { connectButton.performClick() } + } + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click on 'Wallet Connect' button") { + onDetailsScreen { walletConnectButton.clickWithAssertion() } + } + step("Check 'Wallet Connect' screen") { + checkWalletConnectScreen() + } + step("Click on app icon") { + onWalletConnectScreen { appIcon.performClick() } + } + step("Check 'Wallet Connect' details bottom sheet") { + checkWalletConnectDetailsBottomSheet(dAppName) + } + step("Click on 'Disconnect button' is displayed") { + onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } + } + step("Assert connection is not displayed") { + onWalletConnectScreen { appName.assertIsNotDisplayed() } + } + } + } + + @AllureId("3834") + @DisplayName("WC (React App): open session from deeplink not on main screen") + @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") + @Test + fun openWalletConnectSessionNotOnMainScreen() { + val balance = TOTAL_BALANCE + val dAppName = "React App" + val deepLinkUri = getWcUri() + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Create WC session buy deeplink") { + openAppByDeepLink(deepLinkUri) + } + step("Check 'Wallet Connect' bottom sheet") { + checkWalletConnectBottomSheet() + } + step("Click on 'Connect' button") { + onWalletConnectBottomSheet { connectButton.performClick() } + } + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click on 'Wallet Connect' button") { + onDetailsScreen { walletConnectButton.clickWithAssertion() } + } + step("Assert 'Wallet Connect' bottom sheet is displayed") { + onWalletConnectBottomSheet { connectButton.clickWithAssertion() } + } + step("Check 'Wallet Connect' screen") { + checkWalletConnectScreen() + } + step("Click on app icon") { + onWalletConnectScreen { appIcon.performClick() } + } + step("Check 'Wallet Connect' details bottom sheet") { + checkWalletConnectDetailsBottomSheet(dAppName) + } + step("Click on 'Disconnect button' is displayed") { + onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } + } + step("Assert connection is not displayed") { + onWalletConnectScreen { appName.assertIsNotDisplayed() } + } + } + } + + @AllureId("886") + @DisplayName("WC (React App): open session from deeplink ") + @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") + @Test + fun openWalletConnectSession() { + val balance = TOTAL_BALANCE + val dAppName = "React App" + val deepLinkUri = getWcUri() + + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Open recent apps") { + device.uiDevice.pressRecentApps() + } + step("Stop app by swipe") { + swipeUp(startHeightRatio = 0.8f) + } + step("Create WC session buy deeplink") { + openAppByDeepLink(deepLinkUri) + } + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Check 'Wallet Connect' bottom sheet") { + checkWalletConnectBottomSheet() + } + step("Click on 'Connect' button") { + onWalletConnectBottomSheet { connectButton.performClick() } + } + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click on 'Wallet Connect' button") { + onDetailsScreen { walletConnectButton.clickWithAssertion() } + } + step("Check 'Wallet Connect' screen") { + checkWalletConnectScreen() + } + step("Click on app icon") { + onWalletConnectScreen { appIcon.performClick() } + } + step("Check 'Wallet Connect' details bottom sheet") { + checkWalletConnectDetailsBottomSheet(dAppName) + } + step("Click on 'Disconnect button' is displayed") { + onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } + } + step("Assert connection is not displayed") { + onWalletConnectScreen { appName.assertIsNotDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt index 336bec808f..c4f1b6ed48 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags /** * Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons. @@ -54,7 +56,9 @@ fun TangemModalBottomSheetTitle( text = title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag(WalletConnectDetailsBottomSheetTestTags.TITLE), ) } if (subtitle != null) { @@ -62,7 +66,9 @@ fun TangemModalBottomSheetTitle( text = subtitle.resolveReference(), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag(WalletConnectDetailsBottomSheetTestTags.DATE), ) } } @@ -72,7 +78,8 @@ fun TangemModalBottomSheetTitle( onClick = onEndClick, modifier = Modifier .padding(16.dp) - .align(Alignment.CenterEnd), + .align(Alignment.CenterEnd) + .testTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectBottomSheetTestTags.kt new file mode 100644 index 0000000000..8cdcb5f0a3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectBottomSheetTestTags.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.test + +object WalletConnectBottomSheetTestTags { + const val TITLE = "WALLET_CONNECT_BOTTOM_SHEET_TITLE" + const val APP_ICON = "WALLET_CONNECT_BOTTOM_SHEET_APP_ICON" + const val APP_NAME = "WALLET_CONNECT_BOTTOM_SHEET_APP_NAME" + const val APPROVE_ICON = "WALLET_CONNECT_BOTTOM_SHEET_APPROVE_ICON" + const val APP_URL = "WALLET_CONNECT_BOTTOM_SHEET_APP_URL" + + const val CONNECTION_REQUEST_ICON = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_ICON" + const val CONNECTION_REQUEST_TEXT = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_TEXT" + const val CONNECTION_REQUEST_CHEVRON = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_CHEVRON" + + const val WALLET_ICON = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_ICON" + const val WALLET_NAME_TITLE = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_NAME_TITLE" + const val WALLET_NAME = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_NAME" + + const val NETWORKS_ICON = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_ICON" + const val NETWORKS_TITLE = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_TITLE" + const val NETWORKS_SELECTOR_ICON = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_SELECTOR_ICON" + const val NETWORKS_ICONS = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_ICONS" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectDetailsBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectDetailsBottomSheetTestTags.kt new file mode 100644 index 0000000000..f5f7ad7776 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectDetailsBottomSheetTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object WalletConnectDetailsBottomSheetTestTags { + const val TITLE = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_TITLE" + const val DATE = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_DATE" + const val CLOSE_BUTTON = "WALLET_CONNECT_DETAILS_BOTTOM_CLOSE_BUTTON" + const val DISCONNECT_BUTTON = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_DISCONNECT_BUTTON" + const val WALLET_ICON = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET_ICON" + const val WALLET_TITLE = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET" + const val WALLET_NAME = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET_NAME" + const val NETWORKS_TITLE = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET_NAME" + const val NETWORK_ITEM = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_NETWORK_ITEM" + const val NETWORK_ICON = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_NETWORK_ICON" + const val NETWORK_NAME = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_NETWORK_NAME" + const val NETWORK_SYMBOL = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_NETWORK_SYMBOL" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt new file mode 100644 index 0000000000..8280fc5021 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object WalletConnectScreenTestTags { + const val MORE_BUTTON = "WALLET_CONNECT_SCREEN_MORE_BUTTON" + const val WALLET_NAME = "WALLET_CONNECT_SCREEN_WALLET_NAME" + const val APP_ICON = "WALLET_CONNECT_SCREEN_APP_ICON" + const val APP_NAME = "WALLET_CONNECT_SCREEN_APP_NAME" + const val APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON" + const val APP_URL = "WALLET_CONNECT_SCREEN_APP_URL" +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt index 0096e3ef3c..3247db2b9e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -17,6 +18,8 @@ import coil.compose.AsyncImage import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags +import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.impl.R @@ -43,7 +46,8 @@ internal fun WcAppInfoItem( AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size48) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)), + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .testTag(WalletConnectBottomSheetTestTags.APP_ICON), model = iconUrl, contentDescription = title, error = painterResource(R.drawable.img_wc_dapp_icon_placeholder_48), @@ -58,7 +62,9 @@ internal fun WcAppInfoItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.weight(1f, fill = false), + modifier = Modifier + .weight(1f, fill = false) + .testTag(WalletConnectBottomSheetTestTags.APP_NAME), text = title, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h3, @@ -67,7 +73,9 @@ internal fun WcAppInfoItem( ) if (verifiedDAppState is VerifiedDAppState.Verified) { Icon( - modifier = Modifier.size(20.dp), + modifier = Modifier + .size(20.dp) + .testTag(WalletConnectBottomSheetTestTags.APPROVE_ICON), painter = painterResource(R.drawable.img_approvale2_20), contentDescription = null, tint = Color.Unspecified, @@ -78,6 +86,7 @@ internal fun WcAppInfoItem( text = subtitle, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(WalletConnectBottomSheetTestTags.APP_URL), ) } } @@ -86,11 +95,15 @@ internal fun WcAppInfoItem( @Composable internal fun WcNetworkInfoItem(@DrawableRes icon: Int, name: String, symbol: String, modifier: Modifier = Modifier) { Row( - modifier = modifier.padding(vertical = 14.dp, horizontal = 12.dp), + modifier = modifier + .padding(vertical = 14.dp, horizontal = 12.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ITEM), verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ICON), painter = painterResource(icon), contentDescription = null, tint = Color.Unspecified, @@ -98,7 +111,8 @@ internal fun WcNetworkInfoItem(@DrawableRes icon: Int, name: String, symbol: Str Text( modifier = Modifier .padding(start = 12.dp) - .weight(1f, fill = false), + .weight(1f, fill = false) + .testTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_NAME), text = name, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, @@ -106,7 +120,9 @@ internal fun WcNetworkInfoItem(@DrawableRes icon: Int, name: String, symbol: Str overflow = TextOverflow.Ellipsis, ) Text( - modifier = Modifier.padding(start = 4.dp), + modifier = Modifier + .padding(start = 4.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_SYMBOL), text = symbol, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index ebd0adbd3a..0f5487ae09 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -42,6 +43,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList @@ -62,6 +64,7 @@ internal fun WcAppInfoModalBottomSheet(state: WcAppInfoUM, onBack: () -> Unit, o title = resourceReference(R.string.wc_wallet_connect), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, + modifier = Modifier.testTag(WalletConnectBottomSheetTestTags.TITLE), ) }, content = { @@ -157,7 +160,9 @@ private fun WcAppInfoFirstBlock(state: WcAppInfoUM.Content, modifier: Modifier = private fun ConnectionRequestBlock(expanded: Boolean, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_ICON), painter = painterResource(R.drawable.ic_connect_new_24), contentDescription = null, tint = TangemTheme.colors.icon.accent, @@ -165,7 +170,8 @@ private fun ConnectionRequestBlock(expanded: Boolean, modifier: Modifier = Modif Text( modifier = Modifier .padding(start = TangemTheme.dimens.spacing4) - .weight(1f), + .weight(1f) + .testTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_TEXT), text = stringResourceSafe(R.string.wc_connection_request), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, @@ -173,7 +179,8 @@ private fun ConnectionRequestBlock(expanded: Boolean, modifier: Modifier = Modif Icon( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) - .size(width = 18.dp, height = 24.dp), + .size(width = 18.dp, height = 24.dp) + .testTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_CHEVRON), painter = painterResource(if (expanded) R.drawable.ic_chevron_up_24 else R.drawable.ic_chevron_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -319,7 +326,9 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectBottomSheetTestTags.WALLET_ICON), painter = painterResource(R.drawable.ic_wallet_new_24), contentDescription = null, tint = TangemTheme.colors.icon.accent, @@ -330,14 +339,18 @@ private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Mo verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE), text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, ) Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME), text = walletName, textAlign = TextAlign.End, style = TangemTheme.typography.body1, @@ -366,7 +379,9 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectBottomSheetTestTags.NETWORKS_ICON), painter = painterResource(R.drawable.ic_network_new_24), contentDescription = null, tint = TangemTheme.colors.icon.accent, @@ -374,7 +389,8 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier Text( modifier = Modifier .padding(start = TangemTheme.dimens.spacing4) - .weight(1f), + .weight(1f) + .testTag(WalletConnectBottomSheetTestTags.NETWORKS_TITLE), text = stringResourceSafe(R.string.wc_common_networks), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, @@ -385,7 +401,9 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier is WcNetworksInfo.NoneNetworksAdded -> Unit } Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), + modifier = Modifier + .size(width = 18.dp, height = 24.dp) + .testTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON), painter = painterResource(id = R.drawable.ic_select_18_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -417,7 +435,8 @@ private fun NetworkIcons(items: ImmutableList, modifier: Modi ) .padding(2.dp) .clip(CircleShape) - .size(20.dp), + .size(20.dp) + .testTag(WalletConnectBottomSheetTestTags.NETWORKS_ICONS), ) } if (remainingCount > 0) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt index 4a97cf03a1..5fab3966ed 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +29,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -116,7 +118,8 @@ private fun WcConnectedAppInfoBSContent(state: WcConnectedAppInfoUM, modifier: M SecondaryButton( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp), + .padding(vertical = 16.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.DISCONNECT_BUTTON), text = stringResourceSafe(R.string.common_disconnect), enabled = state.disconnectButtonConfig.enabled, showProgress = state.disconnectButtonConfig.showProgress, @@ -142,7 +145,9 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier = verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON), painter = painterResource(R.drawable.ic_wallet_new_24), contentDescription = null, tint = TangemTheme.colors.icon.accent, @@ -150,7 +155,8 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier = Text( modifier = Modifier .padding(start = TangemTheme.dimens.spacing4) - .weight(1f), + .weight(1f) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE), text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, @@ -158,7 +164,8 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier = Text( modifier = Modifier .padding(start = TangemTheme.dimens.spacing16) - .weight(1f), + .weight(1f) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME), text = state.walletName, textAlign = TextAlign.End, style = TangemTheme.typography.body1, @@ -174,7 +181,8 @@ private fun NetworksBlock(networks: ImmutableList, m Text( modifier = Modifier .padding(top = 12.dp, bottom = 4.dp) - .padding(horizontal = 12.dp), + .padding(horizontal = 12.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.NETWORKS_TITLE), text = stringResourceSafe(R.string.wc_connected_networks), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index 25acf5033f..f079b379c0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -35,6 +36,7 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.WalletConnectScreenTestTags import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData import com.tangem.features.walletconnect.impl.R @@ -170,7 +172,9 @@ private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Mod Column(modifier = modifier) { key("${connection.userWalletId}_${connection.walletName}") { Text( - modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp), + modifier = Modifier + .padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) + .testTag(WalletConnectScreenTestTags.WALLET_NAME), text = connection.walletName, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, @@ -199,7 +203,8 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi AsyncImage( modifier = Modifier .size(40.dp) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)), + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .testTag(WalletConnectScreenTestTags.APP_ICON), model = appInfo.iconUrl, error = painterResource(R.drawable.img_wc_dapp_icon_placeholder_48), fallback = painterResource(R.drawable.img_wc_dapp_icon_placeholder_48), @@ -214,7 +219,9 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.weight(1f, fill = false), + modifier = Modifier + .weight(1f, fill = false) + .testTag(WalletConnectScreenTestTags.APP_NAME), text = appInfo.name, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, @@ -223,7 +230,9 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi ) if (appInfo.verifiedState is VerifiedDAppState.Verified) { Icon( - modifier = Modifier.size(20.dp), + modifier = Modifier + .size(20.dp) + .testTag(WalletConnectScreenTestTags.APPROVE_ICON), painter = painterResource(R.drawable.img_approvale2_20), contentDescription = null, tint = Color.Unspecified, @@ -234,6 +243,7 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi text = appInfo.subtitle, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, + modifier = Modifier.testTag(WalletConnectScreenTestTags.APP_URL), ) } } @@ -259,6 +269,7 @@ private fun ConnectionsTopBar( TopAppBarButton( button = config.startButtonUM, tint = TangemTheme.colors.icon.primary1, + modifier = Modifier.testTag(WalletConnectScreenTestTags.MORE_BUTTON), ) }, title = { From 33f03e8653ce58ff4a52e267412b52393ea8d7e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 16:49:41 +0500 Subject: [PATCH 42/48] Updated on 2026-08-14 --- .../tokendetails/TokenDetailsPreviewData.kt | 148 +----------------- .../tokendetails/model/TokenDetailsModel.kt | 52 +----- .../tokendetails/state/TokenDetailsState.kt | 6 +- .../TokenDetailsLoadedBalanceConverter.kt | 17 -- .../TokenDetailsSkeletonStateConverter.kt | 8 - .../state/factory/TokenDetailsStateFactory.kt | 61 -------- .../TokenDetailsLoadedTxHistoryConverter.kt | 48 ------ .../TokenDetailsLoadingTxHistoryConverter.kt | 81 ---------- .../TokenDetailsTxHistoryItemFlowConverter.kt | 103 ------------ ...tailsTxHistoryTransactionStateConverter.kt | 136 ---------------- .../tokendetails/ui/TokenDetailsScreen.kt | 43 +---- 11 files changed, 8 insertions(+), 695 deletions(-) delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 375e60ec26..01040f4abc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -1,14 +1,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import androidx.compose.ui.graphics.Color -import androidx.paging.PagingData import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -18,7 +15,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal @Suppress("LargeClass") @@ -168,130 +164,6 @@ internal object TokenDetailsPreviewData { onRefresh = {}, ) - private val txHistoryItems = listOf( - TxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), - TxHistoryState.TxHistoryItemState.GroupTitle( - title = "Today", - itemKey = "Today", - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "1", - amount = "-0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.OUTGOING, - onClick = {}, - iconRes = R.drawable.ic_arrow_up_24, - title = stringReference(value = "Sending"), - subtitle = stringReference(value = "to: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "2", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_arrow_down_24, - title = stringReference(value = "Receiving"), - subtitle = stringReference(value = "from: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "3", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_doc_24, - title = stringReference(value = "Approving"), - subtitle = stringReference(value = "from: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "4", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_exchange_vertical_24, - title = stringReference(value = "Swapping"), - subtitle = stringReference(value = "contract: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.GroupTitle( - title = "Yesterday", - itemKey = "Yesterday", - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "5", - amount = "-0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.OUTGOING, - onClick = {}, - iconRes = R.drawable.ic_arrow_up_24, - title = stringReference(value = "Sending"), - subtitle = stringReference(value = "to: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "6", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_arrow_down_24, - title = stringReference(value = "Receiving"), - subtitle = stringReference(value = "from: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "7", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_doc_24, - title = stringReference(value = "Approving"), - subtitle = stringReference(value = "from: 33BddS...ga2B"), - timestamp = 0, - ), - ), - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Content( - txHash = "8", - amount = "+0.500913 XLM", - time = "8:41", - status = TransactionState.Content.Status.Confirmed, - direction = TransactionState.Content.Direction.INCOMING, - onClick = {}, - iconRes = R.drawable.ic_exchange_vertical_24, - title = stringReference(value = "Swapping"), - subtitle = stringReference(value = "contract: 33BddS...ga2B"), - timestamp = 0, - ), - ), - ) - val tokenDetailsState_1 = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, @@ -299,13 +171,7 @@ internal object TokenDetailsPreviewData { marketPriceBlockState = marketPriceLoading, stakingBlocksState = stakingLoadingBlock, notifications = persistentListOf(), - txHistoryState = TxHistoryState.Content( - contentItems = MutableStateFlow( - value = TxHistoryState.getDefaultLoadingTransactions {}, - ), - ), dialogConfig = null, - pendingTxs = persistentListOf(), expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, @@ -330,12 +196,7 @@ internal object TokenDetailsPreviewData { ), stakingBlocksState = stakingAvailableBlock, notifications = persistentListOf(), - txHistoryState = TxHistoryState.NotSupported( - onExploreClick = {}, - pendingTransactions = persistentListOf(), - ), dialogConfig = null, - pendingTxs = persistentListOf(), expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, @@ -344,12 +205,5 @@ internal object TokenDetailsPreviewData { isMarketPriceAvailable = true, ) - val tokenDetailsState_3 = tokenDetailsState_2.copy( - txHistoryState = TxHistoryState.Content( - contentItems = MutableStateFlow( - value = PagingData.from(txHistoryItems), - ), - ), - stakingBlocksState = stakingBalanceBlock, - ) + val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 795dbe542e..cf061237a4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -1,7 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model import androidx.compose.runtime.Stable -import androidx.paging.cachedIn import arrow.core.getOrElse import arrow.core.merge import com.arkivanov.decompose.router.slot.SlotNavigation @@ -24,7 +23,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -69,8 +67,6 @@ import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -107,8 +103,6 @@ internal class TokenDetailsModel @Inject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, @@ -177,8 +171,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - symbol = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, ) private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -251,7 +243,6 @@ internal class TokenDetailsModel @Inject constructor( private fun updateContent() { subscribeOnCurrencyStatusUpdates() subscribeOnExpressTransactionsUpdates() - updateTxHistory(refresh = false, showItemsLoading = true, initialUpdating = true) } private fun handleBalanceHiding() { @@ -382,39 +373,8 @@ internal class TokenDetailsModel @Inject constructor( } } - /** - * @param refresh - invalidate cache and get data from remote - * @param showItemsLoading - show loading items placeholder. - */ - private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean, initialUpdating: Boolean = false) { - modelScope.launch { - if (!initialUpdating) { - txHistoryContentUpdateEmitter.triggerUpdate() - } else { - val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - ) - - // if countEither is left, handling error state run inside getLoadingTxHistoryState - if (showItemsLoading || txHistoryItemsCountEither.isLeft()) { - internalUiState.value = stateFactory.getLoadingTxHistoryState( - itemsCountEither = txHistoryItemsCountEither, - pendingTransactions = internalUiState.value.pendingTxs, - ) - } - - txHistoryItemsCountEither.onRight { - val maybeTxHistory = txHistoryItemsUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - refresh = refresh, - ).map { it.cachedIn(modelScope) } - - internalUiState.value = stateFactory.getLoadedTxHistoryState(maybeTxHistory) - } - } - } + private fun updateTxHistory() { + modelScope.launch { txHistoryContentUpdateEmitter.triggerUpdate() } } private fun subscribeOnUpdateStakingInfo(cryptoCurrencyStatus: CryptoCurrencyStatus) { @@ -552,8 +512,7 @@ internal class TokenDetailsModel @Inject constructor( override fun onReloadClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol)) - internalUiState.value = stateFactory.getLoadingTxHistoryState() - updateTxHistory(refresh = true, showItemsLoading = true) + updateTxHistory() } override fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { @@ -800,10 +759,7 @@ internal class TokenDetailsModel @Inject constructor( listOf( async { fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id) }, async { - updateTxHistory( - refresh = true, - showItemsLoading = internalUiState.value.txHistoryState !is TxHistoryState.Content, - ) + updateTxHistory() subscribeOnExpressTransactionsUpdates() }, ).awaitAll() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 594aba1cca..d3a16f09cd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -2,10 +2,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import kotlinx.collections.immutable.ImmutableList @@ -18,10 +16,8 @@ internal data class TokenDetailsState( val marketPriceBlockState: MarketPriceBlockState, val stakingBlocksState: StakingBlockUM?, val notifications: ImmutableList, - val pendingTxs: PersistentList, val expressTxsToDisplay: PersistentList, val expressTxs: PersistentList, - val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: PullToRefreshConfig, val bottomSheetConfig: TangemBottomSheetConfig?, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 03fbaa9901..86527f1f30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -5,7 +5,6 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource @@ -18,29 +17,20 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceTy import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal -@Suppress("LongParameterList") internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val symbol: String, - private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, ) : Converter, TokenDetailsState> { - private val txHistoryItemConverter by lazy { - TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents) - } - override fun convert(value: Either): TokenDetailsState { return value.fold( ifLeft = { convertError() }, @@ -64,7 +54,6 @@ internal class TokenDetailsLoadedBalanceConverter( private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { val state = currentStateProvider() val currencyName = state.marketPriceBlockState.currencySymbol - val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList() return state.copy( tokenBalanceBlockState = getBalanceState( @@ -73,12 +62,6 @@ internal class TokenDetailsLoadedBalanceConverter( ), stakingBlocksState = state.stakingBlocksState, marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), - pendingTxs = pendingTxs, - txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) { - state.txHistoryState.copy(pendingTransactions = pendingTxs) - } else { - state.txHistoryState - }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 3b3acb4d24..ee0b33124a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -4,7 +4,6 @@ import arrow.core.getOrElse import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference @@ -24,7 +23,6 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, @@ -64,14 +62,8 @@ internal class TokenDetailsSkeletonStateConverter( marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), stakingBlocksState = StakingBlockUM.Loading(iconState).takeIf { isSupportedInMobileApp }, notifications = persistentListOf(), - pendingTxs = persistentListOf(), expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), - txHistoryState = TxHistoryState.Content( - contentItems = MutableStateFlow( - value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), - ), - ), dialogConfig = null, pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 22365b4892..cb7d795d31 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -1,14 +1,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory -import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -18,7 +15,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -27,8 +23,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.domain.txhistory.models.TxHistoryListError -import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -36,14 +30,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow @Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class TokenDetailsStateFactory( @@ -54,8 +43,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - symbol: String, - decimals: Int, ) { private val skeletonStateConverter by lazy { @@ -75,8 +62,6 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - symbol = symbol, - decimals = decimals, clickIntents = clickIntents, ) } @@ -88,22 +73,6 @@ internal class TokenDetailsStateFactory( ) } - private val loadingTransactionsStateConverter by lazy { - TokenDetailsLoadingTxHistoryConverter( - currentStateProvider = currentStateProvider, - clickIntents = clickIntents, - ) - } - - private val loadedTxHistoryConverter by lazy { - TokenDetailsLoadedTxHistoryConverter( - currentStateProvider = currentStateProvider, - clickIntents = clickIntents, - symbol = symbol, - decimals = decimals, - ) - } - private val refreshStateConverter by lazy { TokenDetailsRefreshStateConverter( currentStateProvider = currentStateProvider, @@ -147,36 +116,6 @@ internal class TokenDetailsStateFactory( return tokenDetailsButtonsConverter.convert(actions) } - fun getLoadingTxHistoryState(): TokenDetailsState { - return currentStateProvider().copy( - txHistoryState = TxHistoryState.Content( - contentItems = MutableStateFlow( - value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), - ), - ), - ) - } - - fun getLoadingTxHistoryState( - itemsCountEither: Either, - pendingTransactions: List, - ): TokenDetailsState { - return loadingTransactionsStateConverter.convert( - value = TokenDetailsLoadingTxHistoryModel( - historyLoadingState = itemsCountEither, - pendingTransactions = pendingTransactions, - ), - ) - } - - fun getLoadedTxHistoryState( - txHistoryEither: Either>>, - ): TokenDetailsState { - return currentStateProvider().copy( - txHistoryState = loadedTxHistoryConverter.convert(txHistoryEither), - ) - } - fun getStateWithClosedDialog(): TokenDetailsState { val state = currentStateProvider() return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false)) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt deleted file mode 100644 index 7a0e028348..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory - -import androidx.paging.PagingData -import arrow.core.Either -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.txhistory.models.TxHistoryListError -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import kotlinx.coroutines.flow.Flow - -internal class TokenDetailsLoadedTxHistoryConverter( - private val currentStateProvider: Provider, - private val clickIntents: TokenDetailsClickIntents, - symbol: String, - decimals: Int, -) : Converter>>, TxHistoryState> { - - private val txHistoryItemFlowConverter by lazy { - TokenDetailsTxHistoryItemFlowConverter( - currentStateProvider = currentStateProvider, - symbol = symbol, - decimals = decimals, - clickIntents = clickIntents, - ) - } - - override fun convert(value: Either>>): TxHistoryState { - return value.fold(ifLeft = ::convertError, ifRight = ::convert) - } - - private fun convertError(error: TxHistoryListError): TxHistoryState { - return when (error) { - is TxHistoryListError.DataError -> { - TxHistoryState.Error( - onReloadClick = clickIntents::onReloadClick, - onExploreClick = clickIntents::onExploreClick, - ) - } - } - } - - private fun convert(items: Flow>): TxHistoryState { - return txHistoryItemFlowConverter.convert(value = items) - } -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt deleted file mode 100644 index 479a510db6..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory - -import androidx.paging.PagingData -import arrow.core.Either -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -internal class TokenDetailsLoadingTxHistoryConverter( - private val currentStateProvider: Provider, - private val clickIntents: TokenDetailsClickIntents, -) : Converter { - - override fun convert(value: TokenDetailsLoadingTxHistoryModel): TokenDetailsState { - return value.historyLoadingState.fold( - ifLeft = { convertError(error = it, pendingTransactions = value.pendingTransactions) }, - ifRight = ::convert, - ) - } - - private fun convertError( - error: TxHistoryStateError, - pendingTransactions: List, - ): TokenDetailsState { - return currentStateProvider().copy( - txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> TxHistoryState.Empty(clickIntents::onExploreClick) - is TxHistoryStateError.DataError -> TxHistoryState.Error( - onReloadClick = clickIntents::onReloadClick, - onExploreClick = clickIntents::onExploreClick, - ) - is TxHistoryStateError.TxHistoryNotImplemented -> { - TxHistoryState.NotSupported( - pendingTransactions = pendingTransactions.toImmutableList(), - onExploreClick = clickIntents::onExploreClick, - ) - } - }, - ) - } - - private fun convert(value: Int): TokenDetailsState { - val state = currentStateProvider() - - return if (state.txHistoryState is TxHistoryState.Content) { - state.txHistoryState.contentItems.update { - PagingData.from(data = createLoadingItems(value)) - } - state - } else { - val txHistoryContent = TxHistoryState.Content( - contentItems = MutableStateFlow( - value = PagingData.from(data = createLoadingItems(value)), - ), - ) - state.copy(txHistoryState = txHistoryContent) - } - } - - private fun createLoadingItems(size: Int): List { - return buildList { - add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) - (1..size).forEach { - add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) - } - } - } - - data class TokenDetailsLoadingTxHistoryModel( - val historyLoadingState: Either, - val pendingTransactions: List, - ) -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt deleted file mode 100644 index c1cd9f825b..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory - -import androidx.paging.* -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState -import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday -import com.tangem.domain.models.network.TxInfo -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import java.util.UUID - -internal class TokenDetailsTxHistoryItemFlowConverter( - private val currentStateProvider: Provider, - private val symbol: String, - private val decimals: Int, - private val clickIntents: TokenDetailsClickIntents, -) : Converter>, TxHistoryState> { - - private val txHistoryItemConverter by lazy { - TokenDetailsTxHistoryTransactionStateConverter( - symbol = symbol, - decimals = decimals, - clickIntents = clickIntents, - ) - } - - override fun convert(value: Flow>): TxHistoryState { - val state = currentStateProvider() - val txHistoryContent = if (state.txHistoryState is TxHistoryState.Content) { - state.txHistoryState - } else { - TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) - } - // FIXME: TxHistoryRepository should send loading transactions - // [REDACTED_JIRA] - value - .onEach { txHistoryStatePagingData -> - txHistoryContent.contentItems.update { - txHistoryStatePagingData - .map { item -> - // [createTransactionState] returns timestamp without formatting - TxHistoryItemState.Transaction(state = createTransactionState(item)) - } - .insertHeaderItem( - terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, - item = TxHistoryItemState.Title(clickIntents::onExploreClick), - ) - .insertGroupTitle() - } - } - .launchIn(CoroutineScope(Dispatchers.IO)) - - return txHistoryContent - } - - private fun createTransactionState(item: TxInfo): TransactionState { - return txHistoryItemConverter.convert(value = item) - } - - private fun PagingData.insertGroupTitle(): PagingData { - return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> - // Use raw timestamp to get date - - // If [afterDate] is the first transaction in the flow, add the group title - val afterDate = after.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null - if (before is TxHistoryItemState.Title) { - return@insertSeparators TxHistoryItemState.GroupTitle( - title = afterDate, - itemKey = UUID.randomUUID().toString(), - ) - } - - /* - * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in - * the new group - */ - val beforeDate = before.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null - return@insertSeparators if (beforeDate != afterDate) { - TxHistoryItemState.GroupTitle( - title = afterDate, - itemKey = UUID.randomUUID().toString(), - ) - } else { - null - } - } - } - - private fun TxHistoryItemState?.getTimestamp(): Long? { - return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { - val txContent = this.state as TransactionState.Content - txContent.timestamp - } else { - null - } - } -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt deleted file mode 100644 index 74c1021d7c..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory - -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Direction -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.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.toTimeFormat -import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.network.TxInfo.* -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents -import com.tangem.features.tokendetails.impl.R -import com.tangem.utils.StringsSigns.MINUS -import com.tangem.utils.StringsSigns.PLUS -import com.tangem.utils.converter.Converter -import com.tangem.utils.toBriefAddressFormat - -internal class TokenDetailsTxHistoryTransactionStateConverter( - private val symbol: String, - private val decimals: Int, - private val clickIntents: TokenDetailsClickIntents, -) : Converter { - - override fun convert(value: TxInfo): TransactionState { - return createTransactionStateItem(item = value) - } - - @Suppress("LongMethod") - private fun createTransactionStateItem(item: TxInfo): TransactionState { - return TransactionState.Content( - txHash = item.txHash, - amount = item.getAmount(), - time = item.timestampInMillis.toTimeFormat(), - status = item.status.tiUiStatus(), - direction = item.extractDirection(), - iconRes = item.extractIcon(), - title = item.extractTitle(), - subtitle = item.extractSubtitle(), - timestamp = item.timestampInMillis, - onClick = { clickIntents.onTransactionClick(item.txHash) }, - ) - } - - private fun TxInfo.extractIcon(): Int = if (status == TransactionStatus.Failed) { - R.drawable.ic_close_24 - } else { - when (type) { - is TransactionType.Approve -> R.drawable.ic_doc_24 - is TransactionType.Staking.Stake, - is TransactionType.Staking.Vote, - is TransactionType.Staking.Restake, - -> R.drawable.ic_transaction_history_staking_24 - is TransactionType.Staking.ClaimRewards, - -> R.drawable.ic_transaction_history_claim_rewards_24 - is TransactionType.Staking.Unstake, - is TransactionType.Staking.Withdraw, - -> R.drawable.ic_transaction_history_unstaking_24 - is TransactionType.Operation, - is TransactionType.Swap, - is TransactionType.Transfer, - is TransactionType.UnknownOperation, - -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 - } - } - - private fun TxInfo.extractTitle(): TextReference = when (val type = type) { - is TransactionType.Approve -> resourceReference(R.string.common_approval) - is TransactionType.Operation -> stringReference(type.name) - is TransactionType.Swap -> resourceReference(R.string.common_swap) - is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) - is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) - is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) - is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) - is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) - is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) - } - - private fun TxInfo.extractSubtitle(): TextReference = when (val interactionAddress = interactionAddressType) { - is InteractionAddressType.Contract -> resourceReference( - id = R.string.transaction_history_contract_address, - formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), - ) - is InteractionAddressType.Multiple -> resourceReference( - id = if (isOutgoing) { - R.string.transaction_history_transaction_to_address - } else { - R.string.transaction_history_transaction_from_address - }, - formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), - ) - is InteractionAddressType.User -> resourceReference( - id = if (isOutgoing) { - R.string.transaction_history_transaction_to_address - } else { - R.string.transaction_history_transaction_from_address - }, - formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), - ) - is InteractionAddressType.Validator -> resourceReference( - id = R.string.transaction_history_transaction_validator, - formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), - ) - null -> { - TextReference.EMPTY - } - } - - private fun TxInfo.extractDirection() = if (isOutgoing) Direction.OUTGOING else Direction.INCOMING - - private fun TransactionStatus.tiUiStatus() = when (this) { - TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed - TransactionStatus.Failed -> TransactionState.Content.Status.Failed - TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed - } - - private fun TxInfo.getAmount(): String { - if (type is TransactionType.Staking.Vote || - type == TransactionType.Staking.ClaimRewards || - type == TransactionType.Staking.Withdraw - ) { - return "" - } - val prefix = when { - status == TransactionStatus.Failed -> "" - this.amount.isZero() -> "" - else -> if (isOutgoing) MINUS else PLUS - } - return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) } - } -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a64108bc51..14a17ee2e1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag @@ -14,8 +14,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet @@ -26,8 +24,6 @@ import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefres import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TokenDetailsScreenTestTags @@ -46,7 +42,6 @@ import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.entity.TxHistoryUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlin.reflect.KProperty // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @@ -63,11 +58,6 @@ internal fun TokenDetailsScreen( contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { - state.txHistoryState.contentItems.collectAsLazyPagingItems() - } else { - null - } val listState = rememberLazyListState() val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle() val betweenItemsPadding = TangemTheme.dimens.spacing12 @@ -160,14 +150,7 @@ internal fun TokenDetailsScreen( modifier = itemModifier, ) - txHistoryItems( - listState = listState, - txHistoryComponent = txHistoryComponent, - txHistoryComponentState = txHistoryComponentState, - txHistoryState = state.txHistoryState, - txHistoryItems = txHistoryItems, - isBalanceHidden = state.isBalanceHidden, - ) + with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) } } } @@ -189,28 +172,6 @@ internal fun TokenDetailsScreen( } } -@Suppress("LongParameterList") -private fun LazyListScope.txHistoryItems( - listState: LazyListState, - txHistoryComponent: TxHistoryComponent, - txHistoryComponentState: TxHistoryUM?, - txHistoryState: TxHistoryState, - txHistoryItems: LazyPagingItems?, - isBalanceHidden: Boolean, -) { - if (txHistoryComponentState != null) { - with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) } - } else { - txHistoryItems( - state = txHistoryState, - isBalanceHidden = isBalanceHidden, - txHistoryItems = txHistoryItems, - ) - } -} - -private inline operator fun State?.getValue(thisObj: Any?, property: KProperty<*>): T? = this?.value - // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) From e0a8e25779fc17d997879b3954baa91beb680af4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 16:56:18 +0500 Subject: [PATCH 43/48] Updated on 2026-08-14 --- .../tangempay/details/impl/build.gradle.kts | 6 +- .../DefaultTangemPayDetailsComponent.kt | 18 +- .../tangempay/di/TangemPayModelModule.kt | 20 + .../entity/TangemPayDetailsTopBarConfig.kt | 9 + .../tangempay/entity/TangemPayDetailsUM.kt | 35 ++ .../tangempay/model/TangemPayDetailsModel.kt | 61 +++ .../TangemPayDetailsRefreshTransformer.kt | 10 + .../tangempay/ui/TangemPayDetailsScreen.kt | 369 ++++++++++++++++++ 8 files changed, 520 insertions(+), 8 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayDetailsRefreshTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts index 4f9fb1c110..ae6c79993c 100644 --- a/features/tangempay/details/impl/build.gradle.kts +++ b/features/tangempay/details/impl/build.gradle.kts @@ -13,9 +13,9 @@ android { dependencies { /** Core */ + implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.configToggles) /** Features api */ implementation(projects.features.tangempay.details.api) @@ -29,4 +29,8 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt index 2c55cb6a3a..3fe44cd539 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -1,26 +1,30 @@ package com.tangem.features.tangempay.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.features.tangempay.model.TangemPayDetailsModel +import com.tangem.features.tangempay.ui.TangemPayDetailsScreen import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -@Suppress("UnusedPrivateMember") internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: TangemPayDetailsComponent.Params, ) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { + private val model: TangemPayDetailsModel = getOrCreateModel(params = params) + @Composable override fun Content(modifier: Modifier) { - Box(modifier.fillMaxSize().background(Color.Red)) - // TODO("[REDACTED_JIRA]") + val state by model.uiState.collectAsStateWithLifecycle() + NavigationBar3ButtonsScrim() + TangemPayDetailsScreen(state = state, modifier = modifier) } @AssistedFactory diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt new file mode 100644 index 0000000000..a9bee0452a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.tangempay.model.TangemPayDetailsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface TangemPayModelModule { + + @Binds + @IntoMap + @ClassKey(TangemPayDetailsModel::class) + fun bindWcConnectionsModel(model: TangemPayDetailsModel): Model +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt new file mode 100644 index 0000000000..85908dce5c --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import kotlinx.collections.immutable.ImmutableList + +internal data class TangemPayDetailsTopBarConfig( + val onBackClick: () -> Unit, + val items: ImmutableList?, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt new file mode 100644 index 0000000000..dbb7977543 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import kotlinx.collections.immutable.ImmutableList + +internal data class TangemPayDetailsUM( + val topBarConfig: TangemPayDetailsTopBarConfig, + val pullToRefreshConfig: PullToRefreshConfig, + val balanceBlockState: TangemPayDetailsBalanceBlockState, + val cardDetailsUM: TangemPayCardDetailsUM, + val isBalanceHidden: Boolean, +) + +data class TangemPayCardDetailsUM(val number: String, val expiry: String, val cvv: String, val onReveal: () -> Unit) + +internal sealed class TangemPayDetailsBalanceBlockState { + + abstract val actionButtons: ImmutableList + + data class Loading( + override val actionButtons: ImmutableList, + ) : TangemPayDetailsBalanceBlockState() + + data class Content( + override val actionButtons: ImmutableList, + val cryptoBalance: String, + val fiatBalance: String, + val isBalanceFlickering: Boolean, + ) : TangemPayDetailsBalanceBlockState() + + data class Error( + override val actionButtons: ImmutableList, + ) : TangemPayDetailsBalanceBlockState() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt new file mode 100644 index 0000000000..a39519d4a9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -0,0 +1,61 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState +import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState +import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig +import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.transformer.update +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayDetailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private val refreshStateJobHolder = JobHolder() + + @Suppress("MagicNumber", "UnusedPrivateMember") + private fun onRefreshSwipe(refreshState: ShowRefreshState) { + uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = true)) + modelScope.launch { + // simulate update logic + delay(2000) + uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false)) + }.saveIn(refreshStateJobHolder) + } + + private fun getInitialState(): TangemPayDetailsUM { + return TangemPayDetailsUM( + topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = router::pop, items = null), + pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = ::onRefreshSwipe), + balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()), + cardDetailsUM = TangemPayCardDetailsUM( + number = "•••• •••• •••• 1245", + expiry = "••/••", + cvv = "•••", + onReveal = {}, + ), + isBalanceHidden = false, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayDetailsRefreshTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayDetailsRefreshTransformer.kt new file mode 100644 index 0000000000..336a2b3a11 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayDetailsRefreshTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class TangemPayDetailsRefreshTransformer(private val isRefreshing: Boolean) : Transformer { + override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM { + return prevState.copy(pullToRefreshConfig = prevState.pullToRefreshConfig.copy(isRefreshing = isRefreshing)) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt new file mode 100644 index 0000000000..3318cbe1b5 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -0,0 +1,369 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.extensions.orMaskWithStars +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.test.TokenDetailsTopBarTestTags +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState +import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig +import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.utils.StringsSigns.DASH_SIGN +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + topBar = { TangemPayDetailsTopAppBar(config = state.topBarConfig) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + val listState = rememberLazyListState() + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + modifier = Modifier.padding(scaffoldPaddings), + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), + ) { + item( + key = TangemPayDetailsBalanceBlockState::class.java, + content = { + TangemPayDetailsBalanceBlock( + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + state = state.balanceBlockState, + isBalanceHidden = state.isBalanceHidden, + ) + }, + ) + item( + key = TangemPayCardDetailsUM::class.java, + content = { + TangemPayCardDetailsBlock( + modifier = modifier + .padding(top = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + state = state.cardDetailsUM, + ) + }, + ) + } + } + } +} + +// region Balance block +@Composable +internal fun TangemPayDetailsBalanceBlock( + state: TangemPayDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersMedium, + ) + .padding(vertical = 12.dp), + ) { + Text( + modifier = Modifier.padding(start = 12.dp), + text = "Tangem Pay", + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + FiatBalance( + modifier = Modifier.padding(start = 12.dp, top = 4.dp), + state = state, + isBalanceHidden = isBalanceHidden, + ) + CryptoBalance( + modifier = Modifier.padding(start = 12.dp, top = 4.dp), + state = state, + isBalanceHidden = isBalanceHidden, + ) + if (state.actionButtons.isNotEmpty()) { + HorizontalActionChips( + modifier = Modifier.padding(top = 12.dp), + buttons = state.actionButtons, + containerColor = TangemTheme.colors.background.primary, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun FiatBalance( + state: TangemPayDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TangemPayDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size102, + height = TangemTheme.dimens.size32, + ), + ) + is TangemPayDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.fiatBalance.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.h2.applyBladeBrush( + isEnabled = state.isBalanceFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + ) + is TangemPayDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun CryptoBalance( + state: TangemPayDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TangemPayDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size70, + height = TangemTheme.dimens.size16, + ), + ) + is TangemPayDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.cryptoBalance.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.caption2.applyBladeBrush( + isEnabled = state.isBalanceFlickering, + textColor = TangemTheme.colors.text.tertiary, + ), + ) + is TangemPayDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} +// endregion + +// region Card details block +@Composable +private fun TangemPayCardDetailsBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersMedium, + ) + .padding(all = 12.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "Card details", + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + Text( + modifier = Modifier.clickable(onClick = state.onReveal), + text = "Reveal", + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.body2, + ) + } + CardDetailsTextContainer( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + text = state.number, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CardDetailsTextContainer(modifier = Modifier.weight(1f), text = state.expiry) + CardDetailsTextContainer(modifier = Modifier.weight(1f), text = state.cvv) + } + } +} + +@Composable +private fun CardDetailsTextContainer(text: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .heightIn(min = 48.dp) + .background(color = TangemTheme.colors.field.primary, shape = RoundedCornerShape(16.dp)), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), + text = text, + maxLines = 1, + color = TangemTheme.colors.text.disabled, + style = TangemTheme.typography.body2, + ) + } +} + +// endregion + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modifier: Modifier = Modifier) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } + TopAppBar( + modifier = modifier, + navigationIcon = { + IconButton(onClick = config.onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Back", + ) + } + }, + title = {}, + actions = { + AnimatedVisibility(visible = config.items != null && config.items.isNotEmpty()) { + IconButton(onClick = { showDropdownMenu = true }) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vertical_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "More", + modifier = Modifier.testTag(TokenDetailsTopBarTestTags.MORE_BUTTON), + ) + } + } + + TangemDropdownMenu( + expanded = showDropdownMenu, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { showDropdownMenu = false }, + content = { + config.items?.fastForEach { + TangemDropdownItem( + item = it, + dismissParent = { showDropdownMenu = false }, + ) + } + }, + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + titleContentColor = TangemTheme.colors.icon.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayDetailsScreenPreview( + @PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM, +) { + TangemThemePreview { + TangemPayDetailsScreen(state = state) + } +} + +private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemPayDetailsUM( + topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null), + pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}), + balanceBlockState = TangemPayDetailsBalanceBlockState.Content( + actionButtons = persistentListOf( + ActionButtonConfig( + text = resourceReference(id = R.string.common_receive), + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ), + cryptoBalance = "1234.56 USDT", + fiatBalance = "$1234.56", + isBalanceFlickering = false, + ), + cardDetailsUM = TangemPayCardDetailsUM( + number = "•••• •••• •••• 1245", + expiry = "••/••", + cvv = "•••", + onReveal = {}, + ), + isBalanceHidden = false, + ), + TangemPayDetailsUM( + topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null), + pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}), + balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()), + cardDetailsUM = TangemPayCardDetailsUM( + number = "•••• •••• •••• 1245", + expiry = "••/••", + cvv = "•••", + onReveal = {}, + ), + isBalanceHidden = false, + ), + ), +) \ No newline at end of file From aa9eda075833f06bd30fb2bdfc7be690c91d3363 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 21:47:00 +0400 Subject: [PATCH 44/48] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- libs/crypto/build.gradle.kts | 31 +++-- .../derivation/AccountNodeRecognizer.kt | 46 +++++++ .../derivation/MutableDerivationPath.kt | 44 +++++++ .../derivation/AccountNodeRecognizerTest.kt | 116 ++++++++++++++++++ .../derivation/MutableDerivationPathTest.kt | 92 ++++++++++++++ 6 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt create mode 100644 libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt create mode 100644 libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a9c2010b63..75a7c7f036 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1213" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-560" +tangemCardSdk = "develop-561" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 0278a0501c..2adf84b11c 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -10,17 +10,30 @@ android { namespace = "com.tangem.lib.crypto" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { - /** Coroutines */ - implementation(deps.kotlin.coroutines) - - /** SDK */ - implementation(tangemDeps.blockchain) - - /** Core */ + // region Project implementation(projects.core.utils) - - /** Libs */ implementation(projects.libs.blockchainSdk) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) + // endregion + + // region Other deps + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + // endregion + + // region Test libraries + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.truth) + // endregion } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt new file mode 100644 index 0000000000..6008e6ea58 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -0,0 +1,46 @@ +package com.tangem.lib.crypto.derivation + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.isUTXO +import com.tangem.crypto.hdWallet.DerivationPath + +/** + * Utility class to recognize the account node in a derivation path based on the blockchain type. + * Derivation path schema: [ m / purpose' / coin_type' / account' / change / address_index ]. + * + * @param blockchain the blockchain for which the account node is to be recognized + * +[REDACTED_AUTHOR] + */ +class AccountNodeRecognizer(blockchain: Blockchain) { + + /** Index of the account node in the derivation path */ + val accountNodeIndex: Int = if (blockchain.isUTXO) { + UTXO_BLOCKCHAIN_NODE_INDEX + } else { + NON_UTXO_BLOCKCHAIN_NODE_INDEX + } + + /** Recognizes the account node value from the given derivation path string [derivationPathValue] */ + fun recognize(derivationPathValue: String): Long? { + return runCatching { + recognize(derivationPath = DerivationPath(rawPath = derivationPathValue)) + } + .getOrNull() + } + + /** Recognizes the account node value from the given [derivationPath] */ + fun recognize(derivationPath: DerivationPath): Long? { + return runCatching { + val accountNode = derivationPath.nodes.getOrNull(accountNodeIndex) + + accountNode?.getIndex(includeHardened = false) + } + .getOrNull() + } + + private companion object { + const val UTXO_BLOCKCHAIN_NODE_INDEX = 2 + const val NON_UTXO_BLOCKCHAIN_NODE_INDEX = 4 + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt new file mode 100644 index 0000000000..92ec3a5e41 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt @@ -0,0 +1,44 @@ +package com.tangem.lib.crypto.derivation + +import com.tangem.blockchain.common.Blockchain +import com.tangem.crypto.hdWallet.DerivationNode +import com.tangem.crypto.hdWallet.DerivationPath +import timber.log.Timber + +/** Extension function to convert a [DerivationPath] into a [MutableDerivationPath] */ +fun DerivationPath.toMutable(): MutableDerivationPath = MutableDerivationPath(value = this) + +/** + * A mutable representation of a derivation path, allowing modifications to specific nodes + * + * @property value the initial derivationPath to be used + */ +class MutableDerivationPath internal constructor(val value: DerivationPath) { + + /** + * Replaces the account node in the derivation path with a new value + * + * @param value the new index to set for the account node + * @param blockchain the blockchain used to determine the account node index + */ + fun replaceAccountNode(value: Long, blockchain: Blockchain): MutableDerivationPath { + val mutableNodes = this@MutableDerivationPath.value.nodes.toMutableList() + + val accountNodeIndex = AccountNodeRecognizer(blockchain).accountNodeIndex + val accountNode = mutableNodes.getOrNull(accountNodeIndex) + + if (accountNode != null) { + mutableNodes[accountNodeIndex] = when (accountNode) { + is DerivationNode.Hardened -> DerivationNode.Hardened(value) + is DerivationNode.NonHardened -> DerivationNode.NonHardened(value) + } + } else { + Timber.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}") + } + + return DerivationPath(path = mutableNodes).toMutable() + } + + /** Applies the changes and returns it as an immutable [DerivationPath] */ + fun apply(): DerivationPath = value +} \ No newline at end of file diff --git a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt new file mode 100644 index 0000000000..4295568a38 --- /dev/null +++ b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt @@ -0,0 +1,116 @@ +package com.tangem.lib.crypto.derivation + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.crypto.hdWallet.DerivationPath +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +internal class AccountNodeRecognizerTest { + + private val utxoBlockchain = Blockchain.Bitcoin + private val nonUtxoBlockchain = Blockchain.Ethereum + + @Nested + inner class RecognizeAsDerivationPath { + + @Test + fun `returns account node value for UTXO blockchain`() { + // Arrange + val recognizer = AccountNodeRecognizer(utxoBlockchain) + val derivationPath = DerivationPath(rawPath = "m/44'/0'/1'/0/0") + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + val expected = 1 + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `returns account node value for non-UTXO blockchain`() { + // Arrange + val recognizer = AccountNodeRecognizer(nonUtxoBlockchain) + val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/0/0") + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + val expected = 0 + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `returns null if derivation path is shorter than expected`() { + // Arrange + val recognizer = AccountNodeRecognizer(nonUtxoBlockchain) + val derivationPath = DerivationPath(rawPath = "m/44'/0'") + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + Truth.assertThat(actual).isNull() + } + } + + @Nested + inner class RecognizeAsString { + + @Test + fun `returns account node value for UTXO blockchain`() { + // Arrange + val recognizer = AccountNodeRecognizer(utxoBlockchain) + val derivationPath = "m/44'/0'/1'/0/0" + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + val expected = 1 + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `returns account node value for non-UTXO blockchain`() { + // Arrange + val recognizer = AccountNodeRecognizer(nonUtxoBlockchain) + val derivationPath = "m/44'/0'/0'/0/0" + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + val expected = 0 + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `returns null if derivation path is shorter than expected`() { + // Arrange + val recognizer = AccountNodeRecognizer(nonUtxoBlockchain) + val derivationPath = "m/44'/0'" + + // Act + val actual = recognizer.recognize(derivationPath) + + // Assert + Truth.assertThat(actual).isNull() + } + + @Test + fun `returns null if derivation path string is invalid`() { + // Arrange + val recognizer = AccountNodeRecognizer(utxoBlockchain) + val derivationPathValue = "invalid/path" + + // Act + val actual = recognizer.recognize(derivationPathValue) + + // Assert + Truth.assertThat(actual).isNull() + } + } +} \ No newline at end of file diff --git a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt new file mode 100644 index 0000000000..3a364785be --- /dev/null +++ b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/MutableDerivationPathTest.kt @@ -0,0 +1,92 @@ +package com.tangem.lib.crypto.derivation + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.crypto.hdWallet.DerivationPath +import org.junit.jupiter.api.Test + +internal class MutableDerivationPathTest { + + private val utxoBlockchain = Blockchain.Bitcoin + private val nonUtxoBlockchain = Blockchain.Ethereum + + @Test + fun `replaces account node with hardened value`() { + // Arrange + val derivationPath = DerivationPath(rawPath = "m/44'/0'/0'/0/0") + val mutablePath = derivationPath.toMutable() + + // Act + val actual = mutablePath + .replaceAccountNode(value = 1, blockchain = utxoBlockchain) + .apply() + + // Assert + val expected = DerivationPath(rawPath = "m/44'/0'/1'/0/0") + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `replaces account node with non hardened value`() { + // Arrange + val derivationPath = DerivationPath(rawPath = "m/44'/0'/0/0/0") + val mutablePath = derivationPath.toMutable() + + // Act + val actual = mutablePath + .replaceAccountNode(value = 1, blockchain = utxoBlockchain) + .apply() + + // Assert + val expected = DerivationPath(rawPath = "m/44'/0'/1/0/0") + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `does nothing if account node not found`() { + // Arrange + val derivationPath = DerivationPath(rawPath = "m/44'/0'") + val mutablePath = derivationPath.toMutable() + + // Act + val actual = mutablePath + .replaceAccountNode(value = 1, blockchain = utxoBlockchain) + .apply() + + // Assert + val expected = DerivationPath(rawPath = "m/44'/0'") + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `replaces account node with hardened value for non-utxo blockchain`() { + // Arrange + val derivationPath = DerivationPath(rawPath = "m/44'/60'/0'/0/0") + val mutablePath = derivationPath.toMutable() + + // Act + val actual = mutablePath + .replaceAccountNode(value = 2, blockchain = nonUtxoBlockchain) + .apply() + + // Assert + val expected = DerivationPath(rawPath = "m/44'/60'/0'/0/2") + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `does nothing if account node not found for non-utxo blockchain`() { + // Arrange + val derivationPath = DerivationPath(rawPath = "m/44'/60'") + val mutablePath = derivationPath.toMutable() + + // Act + val actual = mutablePath + .replaceAccountNode(value = 2, blockchain = nonUtxoBlockchain) + .apply() + + // Assert + val expected = DerivationPath(rawPath = "m/44'/60'") + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file From f4e72d4e9f3c343603342df4f758804ca0259c67 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Sep 2025 14:45:36 +0300 Subject: [PATCH 45/48] Updated on 2026-08-14 --- fastlane/Fastfile | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 09eae1b14d..efbc818ece 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -32,40 +32,6 @@ platform :android do gradle(task: "testDebugUnitTest") end - - desc "Build release AAB and APK" - lane :buildRelease do |options| - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/release/google-services.json", "../app") - FileUtils.cp("../tangem-android-tools/CI/gradle_properties/build_ci_gradle.properties", "../gradle.properties") - puts File.read("../gradle.properties") - - gradle( - task: "bundle", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - } - ) - gradle( - task: "assemble", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - } - ) - end - - desc "Build internal APK Firebase App Distribution" lane :buildInternal do |options| FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") From 38614dbf4f4483be0a91c8ce48acfeb9003f2a01 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Sep 2025 18:42:40 +0500 Subject: [PATCH 46/48] Updated on 2026-08-14 --- .../entry/AddExistingWalletModel.kt | 27 ++++++++++++++++++- .../entry/WalletActivationModel.kt | 27 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index 328e74cc8c..c562032c4a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -2,9 +2,15 @@ package com.tangem.features.hotwallet.addexistingwallet.entry import com.arkivanov.decompose.router.stack.* import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute @@ -26,6 +32,7 @@ internal class AddExistingWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() @@ -69,13 +76,31 @@ internal class AddExistingWalletModel @Inject constructor( stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) } + private fun showSkipAccessCodeWarningDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(R.string.access_code_alert_skip_description), + title = resourceReference(R.string.access_code_alert_skip_title), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_skip_ok), + onClick = { navigateToPushNotificationsOrNext() }, + ), + dismissOnFirstAction = true, + ), + ) + } + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { override fun onBackClick() { onChildBack() } override fun onSkipClick() { - navigateToPushNotificationsOrNext() + showSkipAccessCodeWarningDialog() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index f163ba6e18..722d2a7480 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -4,10 +4,16 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.core.decompose.di.GlobalUiMessageSender 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.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent @@ -32,6 +38,7 @@ internal class WalletActivationModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { val params = paramsContainer.require() @@ -79,13 +86,31 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) } + private fun showSkipAccessCodeWarningDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(R.string.access_code_alert_skip_description), + title = resourceReference(R.string.access_code_alert_skip_title), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_skip_ok), + onClick = { navigateToPushNotificationsOrNext() }, + ), + dismissOnFirstAction = true, + ), + ) + } + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { override fun onBackClick() { onChildBack() } override fun onSkipClick() { - navigateToPushNotificationsOrNext() + showSkipAccessCodeWarningDialog() } } From ef59ea0e18da5bf23beb3cf86b1e788d9d0f9a69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Sep 2025 10:34:41 +0500 Subject: [PATCH 47/48] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 568 ++++++++++++------------ gradle/tangem_dependencies.toml | 2 +- 2 files changed, 297 insertions(+), 273 deletions(-) diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index ad10260b6b..ebb0fee019 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -1,160 +1,165 @@ { - "coins" : [ + "coins": [ { - "id" : "matic-token", - "symbol" : "MATIC", - "name" : "Polygon", - "networks" : [ + "id": "matic-token", + "symbol": "MATIC", + "name": "Polygon", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0x0000000000000000000000000000000000001010", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0x0000000000000000000000000000000000001010", + "decimalCount": 18 } ] }, { - "id" : "kaspa", - "symbol" : "KAS", - "name" : "Kaspa", - "networks" : [ + "id": "kaspa", + "symbol": "KAS", + "name": "Kaspa", + "networks": [ { - "networkId" : "kaspa/test" + "networkId": "kaspa/test" } ] }, { - "id" : "Dai Stablecoin-DAI", - "symbol" : "DAI", - "name" : "Dai Stablecoin", - "networks" : [ + "id": "Dai Stablecoin-DAI", + "symbol": "DAI", + "name": "Dai Stablecoin", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1", + "decimalCount": 18 }, { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867", + "decimalCount": 18 }, { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0x8a9424745056eb399fd19a0ec26a14316684e274", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0x8a9424745056eb399fd19a0ec26a14316684e274", + "decimalCount": 18 } ] }, { - "id" : "Dummy ERC20-DERC20", - "symbol" : "DERC20", - "name" : "Dummy ERC20", - "networks" : [ + "id": "Dummy ERC20-DERC20", + "symbol": "DERC20", + "name": "Dummy ERC20", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1", + "decimalCount": 18 } ] }, { - "id" : "Ether-ETH", - "symbol" : "ETH", - "name" : "Ether", - "networks" : [ + "id": "Ether-ETH", + "symbol": "ETH", + "name": "Ether", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323", + "decimalCount": 18 }, { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378", + "decimalCount": 18 } ] }, { - "id" : "Test Token-TST", - "symbol" : "TST", - "name" : "Test Token", - "networks" : [ + "id": "Test Token-TST", + "symbol": "TST", + "name": "Test Token", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e", + "decimalCount": 18 } ] }, { - "id" : "tether", - "symbol" : "USDT", - "name" : "Tether USD", - "networks" : [ + "id": "tether", + "symbol": "USDT", + "name": "Tether USD", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803", - "decimalCount" : 6 + "networkId": "ethereum/test", + "contractAddress": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0", + "decimalCount": 18 }, { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0x3813e82e6f7098b9583FC0F33a962D02018B6803", + "decimalCount": 6 }, { - "networkId" : "tron/test", - "contractAddress" : "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj", - "decimalCount" : 6 + "networkId": "binance-smart-chain/test", + "contractAddress": "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd", + "decimalCount": 18 }, { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684", - "decimalCount" : 18 + "networkId": "tron/test", + "contractAddress": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj", + "decimalCount": 6 + }, + { + "networkId": "binance-smart-chain/test", + "contractAddress": "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684", + "decimalCount": 18 } ] }, { - "id" : "just-gov", - "symbol" : "JST", - "name" : "JUST GOV", - "networks" : [ + "id": "just-gov", + "symbol": "JST", + "name": "JUST GOV", + "networks": [ { - "networkId" : "tron/test", - "contractAddress" : "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3", - "decimalCount" : 18 + "networkId": "tron/test", + "contractAddress": "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3", + "decimalCount": 18 } ] }, { - "id" : "Wrapped Ether-WETH", - "symbol" : "WETH", - "name" : "Wrapped Ether", - "networks" : [ + "id": "Wrapped Ether-WETH", + "symbol": "WETH", + "name": "Wrapped Ether", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa", + "decimalCount": 18 } ] }, { - "id" : "Wrapped Matic-WMATIC", - "symbol" : "WMATIC", - "name" : "Wrapped Matic", - "networks" : [ + "id": "Wrapped Matic-WMATIC", + "symbol": "WMATIC", + "name": "Wrapped Matic", + "networks": [ { - "networkId" : "polygon-pos/test", - "contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C", - "decimalCount" : 18 + "networkId": "polygon-pos/test", + "contractAddress": "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + "decimalCount": 18 } ] }, { - "id" : "solana", - "symbol" : "SOL", - "name" : "Solana", - "networks" : [ + "id": "solana", + "symbol": "SOL", + "name": "Solana", + "networks": [ { - "networkId" : "solana/test" + "networkId": "solana/test" } ] }, @@ -162,263 +167,262 @@ "id": "the-open-network", "symbol": "TON", "name": "Toncoin", - "networks": - [ + "networks": [ { "networkId": "the-open-network/test" } ] }, { - "id" : "Tangem Coin A-TCA", - "symbol" : "TCA", - "name" : "Tangem Coin A", - "networks" : [ + "id": "Tangem Coin A-TCA", + "symbol": "TCA", + "name": "Tangem Coin A", + "networks": [ { - "networkId" : "solana/test", - "contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR", - "decimalCount" : 9 + "networkId": "solana/test", + "contractAddress": "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR", + "decimalCount": 9 } ] }, { - "id" : "Tangem Coin B-TCB", - "symbol" : "TCB", - "name" : "Tangem Coin B", - "networks" : [ + "id": "Tangem Coin B-TCB", + "symbol": "TCB", + "name": "Tangem Coin B", + "networks": [ { - "networkId" : "solana/test", - "contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4", - "decimalCount" : 9 + "networkId": "solana/test", + "contractAddress": "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4", + "decimalCount": 9 } ] }, { - "id" : "binancecoin", - "symbol" : "BNB", - "name" : "Binance", - "networks" : [ + "id": "binancecoin", + "symbol": "BNB", + "name": "Binance", + "networks": [ { - "networkId" : "binancecoin/test" + "networkId": "binancecoin/test" }, { - "networkId" : "binance-smart-chain/test" + "networkId": "binance-smart-chain/test" } ] }, { - "id" : "Hemster - 452-HEM", - "symbol" : "HEM", - "name" : "Hemster - 452", - "networks" : [ + "id": "Hemster - 452-HEM", + "symbol": "HEM", + "name": "Hemster - 452", + "networks": [ { - "networkId" : "binancecoin/test", - "contractAddress" : "HEM-452", - "decimalCount" : 8 + "networkId": "binancecoin/test", + "contractAddress": "HEM-452", + "decimalCount": 8 } ] }, { - "id" : "Binance-Peg BTCB Token-BTCB", - "symbol" : "BTCB", - "name" : "Binance-Peg BTCB Token", - "networks" : [ + "id": "Binance-Peg BTCB Token-BTCB", + "symbol": "BTCB", + "name": "Binance-Peg BTCB Token", + "networks": [ { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0x6ce8da28e2f864420840cf74474eff5fd80e65b8", + "decimalCount": 18 } ] }, { - "id" : "Binance-Peg BUSD Token-BUSD", - "symbol" : "BUSD", - "name" : "Binance-Peg BUSD Token", - "networks" : [ + "id": "Binance-Peg BUSD Token-BUSD", + "symbol": "BUSD", + "name": "Binance-Peg BUSD Token", + "networks": [ { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee", + "decimalCount": 18 } ] }, { - "id" : "Binance-Peg USDC Token-USDC", - "symbol" : "USDC", - "name" : "Binance-Peg USDC Token", - "networks" : [ + "id": "Binance-Peg USDC Token-USDC", + "symbol": "USDC", + "name": "Binance-Peg USDC Token", + "networks": [ { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0x64544969ed7ebf5f083679233325356ebe738930", + "decimalCount": 18 } ] }, { - "id" : "Binance-Peg XRP-XRP", - "symbol" : "XRP", - "name" : "Binance-Peg XRP", - "networks" : [ + "id": "Binance-Peg XRP-XRP", + "symbol": "XRP", + "name": "Binance-Peg XRP", + "networks": [ { - "networkId" : "binance-smart-chain/test", - "contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9", - "decimalCount" : 18 + "networkId": "binance-smart-chain/test", + "contractAddress": "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9", + "decimalCount": 18 } ] }, { - "id" : "ethereum", - "symbol" : "ETH", - "name" : "Ethereum", - "networks" : [ + "id": "ethereum", + "symbol": "ETH", + "name": "Ethereum", + "networks": [ { - "networkId" : "ethereum/test" + "networkId": "ethereum/test" } ] }, { - "id" : "ethereum-classic", - "symbol" : "ETC", - "name" : "Ethereum Classic", - "networks" : [ + "id": "ethereum-classic", + "symbol": "ETC", + "name": "Ethereum Classic", + "networks": [ { - "networkId" : "ethereum-classic/test" + "networkId": "ethereum-classic/test" } ] }, { - "id" : "Weenus-WEENUS", - "symbol" : "WEENUS", - "name" : "Weenus", - "networks" : [ + "id": "Weenus-WEENUS", + "symbol": "WEENUS", + "name": "Weenus", + "networks": [ { - "networkId" : "ethereum/test", - "contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5", - "decimalCount" : 18 + "networkId": "ethereum/test", + "contractAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5", + "decimalCount": 18 } ] }, { - "id" : "Xeenus-XEENUS", - "symbol" : "XEENUS", - "name" : "Xeenus", - "networks" : [ + "id": "Xeenus-XEENUS", + "symbol": "XEENUS", + "name": "Xeenus", + "networks": [ { - "networkId" : "ethereum/test", - "contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c", - "decimalCount" : 18 + "networkId": "ethereum/test", + "contractAddress": "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c", + "decimalCount": 18 } ] }, { - "id" : "Yeenus-YEENUS", - "symbol" : "YEENUS", - "name" : "Yeenus", - "networks" : [ + "id": "Yeenus-YEENUS", + "symbol": "YEENUS", + "name": "Yeenus", + "networks": [ { - "networkId" : "ethereum/test", - "contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7", - "decimalCount" : 8 + "networkId": "ethereum/test", + "contractAddress": "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7", + "decimalCount": 8 } ] }, { - "id" : "Zeenus-ZEENUS", - "symbol" : "ZEENUS", - "name" : "Zeenus", - "networks" : [ + "id": "Zeenus-ZEENUS", + "symbol": "ZEENUS", + "name": "Zeenus", + "networks": [ { - "networkId" : "ethereum/test", - "contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e", - "decimalCount" : 0 + "networkId": "ethereum/test", + "contractAddress": "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e", + "decimalCount": 0 } ] }, { - "id" : "avalanche-2", - "symbol" : "AVAX", - "name" : "Avalanche", - "networks" : [ + "id": "avalanche-2", + "symbol": "AVAX", + "name": "Avalanche", + "networks": [ { - "networkId" : "avalanche/test" + "networkId": "avalanche/test" } ] }, { - "id" : "The Fuji stablecoin-FUJISTABLE", - "symbol" : "FUJISTABLE", - "name" : "The Fuji stablecoin", - "networks" : [ + "id": "The Fuji stablecoin-FUJISTABLE", + "symbol": "FUJISTABLE", + "name": "The Fuji stablecoin", + "networks": [ { - "networkId" : "avalanche/test", - "contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22", - "decimalCount" : 6 + "networkId": "avalanche/test", + "contractAddress": "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22", + "decimalCount": 6 } ] }, { - "id" : "To the Moon-FUJIMOON", - "symbol" : "FUJIMOON", - "name" : "To the Moon", - "networks" : [ + "id": "To the Moon-FUJIMOON", + "symbol": "FUJIMOON", + "name": "To the Moon", + "networks": [ { - "networkId" : "avalanche/test", - "contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668", - "decimalCount" : 9 + "networkId": "avalanche/test", + "contractAddress": "0x97132C109c6816525F7f338DCb7435E1412A7668", + "decimalCount": 9 } ] }, { - "id" : "fantom", - "symbol" : "FTM", - "name" : "Fantom", - "networks" : [ + "id": "fantom", + "symbol": "FTM", + "name": "Fantom", + "networks": [ { - "networkId" : "fantom/test" + "networkId": "fantom/test" } ] }, { - "id" : "Fantom USD-FUSD", - "symbol" : "FUSD", - "name" : "Fantom USD", - "networks" : [ + "id": "Fantom USD-FUSD", + "symbol": "FUSD", + "name": "Fantom USD", + "networks": [ { - "networkId" : "fantom/test", - "contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074", - "decimalCount" : 18 + "networkId": "fantom/test", + "contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074", + "decimalCount": 18 } ] }, { - "id" : "Wrapped Fantom-WFTM", - "symbol" : "WFTM", - "name" : "Wrapped Fantom", - "networks" : [ + "id": "Wrapped Fantom-WFTM", + "symbol": "WFTM", + "name": "Wrapped Fantom", + "networks": [ { - "networkId" : "fantom/test", - "contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621", - "decimalCount" : 18 + "networkId": "fantom/test", + "contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621", + "decimalCount": 18 } ] }, { - "id" : "bitcoin", - "symbol" : "BTC", - "name" : "Bitcoin", - "networks" : [ + "id": "bitcoin", + "symbol": "BTC", + "name": "Bitcoin", + "networks": [ { - "networkId" : "bitcoin/test" + "networkId": "bitcoin/test" } ] }, { - "id" : "vechain", - "symbol" : "VET", - "name" : "VeChain", - "networks" : [ + "id": "vechain", + "symbol": "VET", + "name": "VeChain", + "networks": [ { - "networkId" : "vechain/test" + "networkId": "vechain/test" } ] }, @@ -435,34 +439,34 @@ ] }, { - "id" : "tron", - "symbol" : "TRX", - "name" : "Tron", - "networks" : [ - { - "networkId" : "tron/test" - } - ] + "id": "tron", + "symbol": "TRX", + "name": "Tron", + "networks": [ + { + "networkId": "tron/test" + } + ] }, { - "id" : "algorand", - "symbol" : "ALGO", - "name" : "Algorand", - "networks" : [ - { - "networkId" : "algorand/test" - } - ] + "id": "algorand", + "symbol": "ALGO", + "name": "Algorand", + "networks": [ + { + "networkId": "algorand/test" + } + ] }, { - "id" : "arbitrum-one", - "symbol" : "ETH", - "name" : "Arbitrum", - "networks" : [ - { - "networkId" : "arbitrum-one/test" - } - ] + "id": "arbitrum-one", + "symbol": "ETH", + "name": "Arbitrum", + "networks": [ + { + "networkId": "arbitrum-one/test" + } + ] }, { "id": "stellar", @@ -485,12 +489,12 @@ ] }, { - "id" : "polkadot", - "symbol" : "DOT", - "name" : "Polkadot", - "networks" : [ + "id": "polkadot", + "symbol": "DOT", + "name": "Polkadot", + "networks": [ { - "networkId" : "polkadot/test" + "networkId": "polkadot/test" } ] }, @@ -509,8 +513,7 @@ "id": "kava", "symbol": "KAVA", "name": "Kava EVM", - "networks": - [ + "networks": [ { "networkId": "kava/test" } @@ -520,8 +523,7 @@ "id": "telos", "symbol": "TLOS", "name": "Telos EVM", - "networks": - [ + "networks": [ { "networkId": "telos/test" } @@ -531,8 +533,7 @@ "id": "ravencoin", "symbol": "RVN", "name": "Ravencoin", - "networks": - [ + "networks": [ { "networkId": "ravencoin/test" } @@ -542,8 +543,7 @@ "id": "cosmos", "symbol": "ATOM", "name": "Cosmos Hub", - "networks": - [ + "networks": [ { "networkId": "cosmos/test" } @@ -798,6 +798,30 @@ "networkId": "alephium/test" } ] + }, + { + "id": "chainlink", + "name": "Chainlink", + "symbol": "LINK", + "networks": [ + { + "networkId": "ethereum/test", + "contractAddress": "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5", + "decimalCount": 18 + } + ] + }, + { + "id": "gho", + "name": "GHO", + "symbol": "GHO", + "networks": [ + { + "networkId": "ethereum/test", + "contractAddress": "0xc4bF5CbDaBE595361438F8c6a187bDc330539c60", + "decimalCount": 18 + } + ] } ] } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 75a7c7f036..72c784bae5 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1213" +tangemBlockchainSdk = "develop-1216" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-561" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 39c859f73aa7e8751ff35d241d617b51c4b3fe8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Sep 2025 09:55:24 +0400 Subject: [PATCH 48/48] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 13 -- .../tap/di/domain/TokensDomainModule.kt | 33 +---- .../tap/di/domain/TransactionDomainModule.kt | 6 - .../tokens/DefaultTokensFeatureToggles.kt | 8 +- .../configs/feature_toggles_config.json | 4 - .../di/WalletConnectDataModule.kt | 10 -- .../pair/AssociateNetworksDelegate.kt | 16 +-- .../utils/WcNetworksConverter.kt | 16 +-- .../domain/nft/DisableWalletNFTUseCase.kt | 18 +-- .../domain/nft/FetchNFTCollectionsUseCase.kt | 18 +-- .../tangem/domain/nft/RefreshAllNFTUseCase.kt | 18 +-- .../tokens/AddCryptoCurrenciesUseCase.kt | 15 +-- .../tokens/ApplyTokenListSortingUseCase.kt | 13 +- .../tokens/FetchCurrencyStatusUseCase.kt | 15 +-- ...GetBalanceNotEnoughForFeeWarningUseCase.kt | 13 +- .../domain/tokens/GetCryptoCurrencyUseCase.kt | 19 +-- .../tokens/GetCurrencyWarningsUseCase.kt | 13 +- .../IsCryptoCurrencyCoinCouldHideUseCase.kt | 18 +-- ...RefreshMultiCurrencyWalletQuotesUseCase.kt | 19 +-- .../domain/tokens/RemoveCurrencyUseCase.kt | 18 +-- .../domain/tokens/TokensFeatureToggles.kt | 5 +- .../BaseCurrencyStatusOperations.kt | 51 +++---- .../CachedCurrenciesStatusesOperations.kt | 127 +----------------- .../ApplyTokenListSortingUseCaseTest.kt | 1 - .../usecase/AssociateAssetUseCase.kt | 30 ++--- .../send/v2/sendnft/model/NFTSendModel.kt | 23 ++-- .../deeplink/DefaultStakingDeepLinkHandler.kt | 21 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 13 +- .../DefaultTokenDetailsDeepLinkHandler.kt | 25 ++-- .../wallet/child/wallet/model/WalletModel.kt | 44 ++---- .../model/intents/WalletClickIntents.kt | 45 +------ 31 files changed, 142 insertions(+), 546 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 4d82335720..d16fc22bd8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -6,7 +6,6 @@ import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -33,29 +32,21 @@ internal object NFTDomainModule { @Provides @Singleton fun providesFetchNFTCollectionsUseCase( - currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): FetchNFTCollectionsUseCase = FetchNFTCollectionsUseCase( - currenciesRepository = currenciesRepository, nftRepository = nftRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) @Provides @Singleton fun providesRefreshAllNFTUseCase( - currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): RefreshAllNFTUseCase = RefreshAllNFTUseCase( - currenciesRepository = currenciesRepository, nftRepository = nftRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) @Provides @@ -127,16 +118,12 @@ internal object NFTDomainModule { fun provideDisableWalletNFTUseCase( walletsRepository: WalletsRepository, nftRepository: NFTRepository, - currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): DisableWalletNFTUseCase { return DisableWalletNFTUseCase( walletsRepository = walletsRepository, nftRepository = nftRepository, - currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index bdbe9037cc..726ee06efc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -47,7 +47,6 @@ internal object TokensDomainModule { multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, stakingIdFactory: StakingIdFactory, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( @@ -56,7 +55,6 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, stakingIdFactory = stakingIdFactory, ) } @@ -111,13 +109,11 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): RemoveCurrencyUseCase { return RemoveCurrencyUseCase( currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -156,7 +152,6 @@ internal object TokensDomainModule { dispatchers: CoroutineDispatcherProvider, baseCurrencyStatusOperations: BaseCurrencyStatusOperations, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, @@ -165,7 +160,6 @@ internal object TokensDomainModule { currencyChecksRepository = currencyChecksRepository, currencyStatusOperations = baseCurrencyStatusOperations, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -177,7 +171,6 @@ internal object TokensDomainModule { multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, stakingIdFactory: StakingIdFactory, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( @@ -186,7 +179,6 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, stakingIdFactory = stakingIdFactory, ) } @@ -214,9 +206,8 @@ internal object TokensDomainModule { fun provideGetCryptoCurrencyUseCase( currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): GetCryptoCurrencyUseCase { - return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles) + return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier) } @Provides @@ -238,13 +229,11 @@ internal object TokensDomainModule { fun provideApplyTokenListSortingUseCase( currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): ApplyTokenListSortingUseCase { return ApplyTokenListSortingUseCase( currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, dispatchers = dispatchers, ) } @@ -304,14 +293,10 @@ internal object TokensDomainModule { @Provides @Singleton fun provideIsCryptoCurrencyCoinCouldHideUseCase( - currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): IsCryptoCurrencyCoinCouldHideUseCase { return IsCryptoCurrencyCoinCouldHideUseCase( - currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -330,13 +315,11 @@ internal object TokensDomainModule { fun provideGetBalanceNotEnoughForFeeWarningUseCase( currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetBalanceNotEnoughForFeeWarningUseCase { return GetBalanceNotEnoughForFeeWarningUseCase( currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, dispatchers = dispatchers, ) } @@ -377,16 +360,12 @@ internal object TokensDomainModule { @Provides @Singleton fun provideRefreshMultiCurrencyWalletQuotesUseCase( - currenciesRepository: CurrenciesRepository, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): RefreshMultiCurrencyWalletQuotesUseCase { return RefreshMultiCurrencyWalletQuotesUseCase( - currenciesRepository = currenciesRepository, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -402,18 +381,13 @@ internal object TokensDomainModule { @Provides @Singleton fun provideBaseCurrencyStatusOperations( - tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { @@ -422,15 +396,10 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, stakingIdFactory = stakingIdFactory, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 0b8802198c..96c968aa66 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -6,8 +6,6 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.WalletAddressServiceRepository @@ -63,18 +61,14 @@ internal object TransactionDomainModule { fun provideAssociateAssetUseCase( cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): AssociateAssetUseCase { return AssociateAssetUseCase( cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 49ac95c749..444b1f4032 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -4,9 +4,5 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.tokens.TokensFeatureToggles internal class DefaultTokensFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : TokensFeatureToggles { - - override val isWalletBalanceFetcherEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED") -} \ No newline at end of file + @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, +) : TokensFeatureToggles \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 3cf7a9e285..40b69242c9 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -43,10 +43,6 @@ "name": "SEND_REDESIGN_ENABLED", "version": "5.28.0" }, - { - "name": "WALLET_BALANCE_FETCHER_ENABLED", - "version": "5.27.0" - }, { "name": "HOT_WALLET_ENABLED", "version": "undefined" diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 0ee7dab28e..0fe3412528 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -21,8 +21,6 @@ import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.WcRequestUseCaseFactory @@ -177,15 +175,11 @@ internal object WalletConnectDataModule { fun wcNetworksConverter( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) @Provides @@ -193,15 +187,11 @@ internal object WalletConnectDataModule { fun associateNetworksDelegate( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, getWallets: GetWalletsUseCase, - currenciesRepository: CurrenciesRepository, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): AssociateNetworksDelegate = AssociateNetworksDelegate( namespaceConverters = namespaceConverters, getWallets = getWallets, - currenciesRepository = currenciesRepository, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index d0f06c74b2..705efb5df5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -10,8 +10,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -19,9 +17,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase internal class AssociateNetworksDelegate( private val namespaceConverters: Set, private val getWallets: GetWalletsUseCase, - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { @Throws(WcPairError.UnsupportedBlockchains::class) @@ -96,14 +92,10 @@ internal class AssociateNetworksDelegate( } private suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), + ) + .orEmpty() .filterIsInstance() .map(CryptoCurrency.Coin::network) // flatten all derivation diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index a7d1d285fb..9d998db372 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -9,8 +9,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -20,9 +18,7 @@ import javax.inject.Inject internal class WcNetworksConverter @Inject constructor( private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { fun createNetwork(chainId: String, wallet: UserWallet): Network? { @@ -101,12 +97,10 @@ internal class WcNetworksConverter @Inject constructor( } private suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ).orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - }.filterIsInstance().map(CryptoCurrency.Coin::network) + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), + ) + .orEmpty() + .filterIsInstance().map(CryptoCurrency.Coin::network) } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt index 32f37d3fe6..c671f030a9 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt @@ -1,32 +1,24 @@ package com.tangem.domain.nft +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository class DisableWalletNFTUseCase( private val walletsRepository: WalletsRepository, private val nftRepository: NFTRepository, - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId) { walletsRepository.disableNFT(userWalletId) - val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId) - } + val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() val networks = currencies.map { it.network } nftRepository.clearCache(userWalletId, networks) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt index 421ae063af..b2b8d20503 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt @@ -1,28 +1,20 @@ package com.tangem.domain.nft +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId class FetchNFTCollectionsUseCase( - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val nftRepository: NFTRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId) { - val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() nftRepository.refreshCollections(userWalletId, currencies.map { it.network }.distinct()) } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt index b274246dcc..a3cba338d2 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt @@ -1,29 +1,21 @@ package com.tangem.domain.nft import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId class RefreshAllNFTUseCase( - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val nftRepository: NFTRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either = Either.catch { - val currencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + val currencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() nftRepository.refreshAll(userWalletId, currencies.map { it.network }.distinct()) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 06b498b2ab..7223a6faae 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -33,7 +33,6 @@ class AddCryptoCurrenciesUseCase( private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, - private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -92,15 +91,11 @@ class AddCryptoCurrenciesUseCase( ): Either = either { val existingCurrencies = catch( block = { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - .toList() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() + .toList() }, catch = ::raise, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 8fbd310f2f..1bad208906 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.withContext class ApplyTokenListSortingUseCase( private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -88,14 +87,10 @@ class ApplyTokenListSortingUseCase( private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): List { val tokens = catch( block = { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh = false) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() }, catch = { raise(TokenListSortingError.DataError(it)) }, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index e500c445fb..5c7ce2a444 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -35,7 +35,6 @@ class FetchCurrencyStatusUseCase( private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, - private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -97,15 +96,11 @@ class FetchCurrencyStatusUseCase( ): CryptoCurrency { return catch( block = { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.firstOrNull { it.id == id } - ?: error("Unable to find currency with ID: $id") - } else { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId = userWalletId, id = id) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.firstOrNull { it.id == id } + ?: error("Unable to find currency with ID: $id") }, ) { raise(CurrencyStatusError.DataError(it)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt index bc8e4fa72d..26ed8b9867 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -26,7 +26,6 @@ import java.math.BigDecimal class GetBalanceNotEnoughForFeeWarningUseCase( private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { suspend operator fun invoke( @@ -71,14 +70,10 @@ class GetBalanceNotEnoughForFeeWarningUseCase( tokenStatus: CryptoCurrencyStatus, feePaidToken: FeePaidCurrency.Token, ): CryptoCurrencyWarning { - val tokens = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + val tokens = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() val token = tokens.find { it is CryptoCurrency.Token && diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 67780eb049..15b876dd8f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -5,16 +5,15 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.repository.CurrenciesRepository class GetCryptoCurrencyUseCase( private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -55,15 +54,11 @@ class GetCryptoCurrencyUseCase( ): CryptoCurrency { return catch( block = { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.firstOrNull { it.id.value == id } - ?: error("Unable to find currency with ID: $id") - } else { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.firstOrNull { it.id.value == id } + ?: error("Unable to find currency with ID: $id") }, catch = { raise(CurrencyStatusError.DataError(it)) }, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 6b64410367..d4611e9d40 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -29,7 +29,6 @@ class GetCurrencyWarningsUseCase( private val currencyChecksRepository: CurrencyChecksRepository, private val currencyStatusOperations: BaseCurrencyStatusOperations, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -152,14 +151,10 @@ class GetCurrencyWarningsUseCase( tokenStatus: CryptoCurrencyStatus, feePaidToken: FeePaidCurrency.Token, ): CryptoCurrencyWarning { - val tokens = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + val tokens = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() val token = tokens.find { it is CryptoCurrency.Token && diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsCryptoCurrencyCoinCouldHideUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsCryptoCurrencyCoinCouldHideUseCase.kt index 5a0da52389..5ea612a931 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsCryptoCurrencyCoinCouldHideUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsCryptoCurrencyCoinCouldHideUseCase.kt @@ -1,27 +1,17 @@ package com.tangem.domain.tokens import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.models.wallet.UserWalletId class IsCryptoCurrencyCoinCouldHideUseCase( - private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyCoin: CryptoCurrency.Coin): Boolean { - return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync( - userWalletId = userWalletId, - refresh = false, - ) - } + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() .none { it is CryptoCurrency.Token && it.network == cryptoCurrencyCoin.network } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt index ce4c325973..2fc30f9448 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -5,19 +5,16 @@ import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.error.QuotesError -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope class RefreshMultiCurrencyWalletQuotesUseCase( - private val currenciesRepository: CurrenciesRepository, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { @@ -41,15 +38,11 @@ class RefreshMultiCurrencyWalletQuotesUseCase( return either { catch( block = { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - .toList() - } else { - currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() + .toList() }, catch = ::raise, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt index 95fec7162c..59381c5e65 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -4,16 +4,15 @@ import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.remove.RemoveCurrencyError import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWalletId class RemoveCurrencyUseCase( private val currenciesRepository: CurrenciesRepository, private val walletManagersFacade: WalletManagersFacade, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -46,17 +45,10 @@ class RemoveCurrencyUseCase( suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { return when (currency) { is CryptoCurrency.Coin -> { - val walletCurrencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync( - userWalletId = userWalletId, - refresh = false, - ) - } + val walletCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index 5add2d0b65..a2eeb1e0a6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -5,7 +5,4 @@ package com.tangem.domain.tokens * [REDACTED_AUTHOR] */ -interface TokensFeatureToggles { - - val isWalletBalanceFetcherEnabled: Boolean -} \ No newline at end of file +interface TokensFeatureToggles \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 3a258c4cf6..55d53e54c8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -28,7 +28,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -53,7 +52,6 @@ abstract class BaseCurrencyStatusOperations( private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, - private val tokensFeatureToggles: TokensFeatureToggles, ) { protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() @@ -62,13 +60,6 @@ abstract class BaseCurrencyStatusOperations( protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> - protected abstract suspend fun fetchComponents( - userWalletId: UserWalletId, - networks: Set, - currenciesIds: Set, - currencies: List, - ): Either - suspend fun getCurrencyStatusFlow( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, @@ -262,14 +253,10 @@ abstract class BaseCurrencyStatusOperations( return either { catch( block = { - val nonEmptyCurrencies = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.toNonEmptyListOrNull() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() - } + val nonEmptyCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.toNonEmptyListOrNull() ?: return emptyList().right() val (_, currenciesIds) = getIds(nonEmptyCurrencies) @@ -332,15 +319,11 @@ abstract class BaseCurrencyStatusOperations( currencyId: CryptoCurrency.ID, ): CryptoCurrency { return Either.catch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.firstOrNull { it.id == currencyId } - ?: error("Unable to find currency with ID: $currencyId") - } else { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId = userWalletId, id = currencyId) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.firstOrNull { it.id == currencyId } + ?: error("Unable to find currency with ID: $currencyId") } .mapLeft(Error::DataError) .bind() @@ -382,16 +365,12 @@ abstract class BaseCurrencyStatusOperations( derivationPath: Network.DerivationPath, ): CryptoCurrency { return Either.catch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.filterIsInstance() - ?.firstOrNull { it.network.id == networkId } - ?: error("Unable to create network coin with ID: $networkId") - } else { - currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) - } + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.filterIsInstance() + ?.firstOrNull { it.network.id == networkId && it.network.derivationPath == derivationPath } + ?: error("Unable to create network coin with ID: $networkId") } .mapLeft { Error.DataError(it) } .bind() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 70700beea8..ef89e15b90 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.operations import arrow.core.* -import arrow.core.raise.either import arrow.core.raise.recover import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow @@ -18,31 +17,27 @@ import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress import com.tangem.utils.extensions.addOrReplace -import com.tangem.utils.extensions.isSingleItem -import kotlinx.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch @Suppress("LongParameterList", "LargeClass") class CachedCurrenciesStatusesOperations( @@ -50,16 +45,11 @@ class CachedCurrenciesStatusesOperations( quotesRepository: QuotesRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, - private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -70,7 +60,6 @@ class CachedCurrenciesStatusesOperations( multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, - tokensFeatureToggles = tokensFeatureToggles, ) { override fun getCurrenciesStatuses( @@ -90,20 +79,6 @@ class CachedCurrenciesStatusesOperations( ): LceFlow> = lceFlow { val prevStatuses = MutableStateFlow(value = emptyList()) - val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull() - - if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId) && - nonEmptyCurrencies != null - ) { - launch { - setFetchStarted(userWalletId) - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) - } - .invokeOnCompletion { setFetchFinished(userWalletId) } - } - currenciesFlow.flatMapLatest { maybeCurrencies -> val currencies = maybeCurrencies .getOrElse { return@flatMapLatest flowOf(it.lceError()) } @@ -154,15 +129,6 @@ class CachedCurrenciesStatusesOperations( ) } - if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId)) { - launch { - setFetchStarted(userWalletId) - - fetchComponents(userWalletId, networks, currenciesIds, currencies) - } - .invokeOnCompletion { setFetchFinished(userWalletId) } - } - val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) combine( @@ -183,11 +149,7 @@ class CachedCurrenciesStatusesOperations( getYieldsBalancesUpdates(userWalletId, currenciesAddresses) }, - flow4 = fetchingState.map { - val state = it[userWalletId] ?: return@map false - - !state.isFinished() - }, + flow4 = flowOf(value = false), transform = ::createCurrenciesStatuses, ) .distinctUntilChanged() @@ -200,55 +162,6 @@ class CachedCurrenciesStatusesOperations( .launchIn(scope = this) } - override suspend fun fetchComponents( - userWalletId: UserWalletId, - networks: Set, - currenciesIds: Set, - currencies: List, - ): Either = either { - coroutineScope { - awaitAll( - async { - if (networks.isSingleItem()) { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = networks.first(), - ), - ) - } else { - multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = networks, - ), - ) - } - }, - async { - val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), - ) - }, - async { - val stakingIds = currencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - stakingIds = stakingIds, - ), - ) - }, - ) - } - .map { } - } - private fun createCurrenciesStatuses( currencies: NonEmptyList, maybeQuotes: Either>?, @@ -428,36 +341,4 @@ class CachedCurrenciesStatusesOperations( } .distinctUntilChanged() } - - private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { - return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } == true - } - - private fun setFetchStarted(userWalletId: UserWalletId) { - fetchingState.update { - it.toMutableMap().apply { - put(key = userWalletId, value = FetchingState.STARTED) - } - } - } - - private fun setFetchFinished(userWalletId: UserWalletId) { - fetchingState.update { - it.toMutableMap().apply { - put(key = userWalletId, value = FetchingState.FINISHED) - } - } - } - - enum class FetchingState { - STARTED, FINISHED; - - fun isStarted() = this == STARTED - fun isFinished() = this == FINISHED - } - - companion object { - - private val fetchingState = MutableStateFlow(value = emptyMap()) - } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index b0025a5e46..be67de0762 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -188,7 +188,6 @@ internal class ApplyTokenListSortingUseCaseTest { currenciesRepository = tokensRepository, dispatchers = TestingCoroutineDispatcherProvider(), multiWalletCryptoCurrenciesSupplier = mockk(), - tokensFeatureToggles = mockk(), ) private fun getTokensRepository( diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt index 322f65b335..f528a1c75a 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -12,8 +12,6 @@ import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.isNullOrZero @@ -22,10 +20,8 @@ import kotlinx.coroutines.flow.firstOrNull class AssociateAssetUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -33,25 +29,19 @@ class AssociateAssetUseCase( currency: CryptoCurrency, ): Either { return either { - val networkCoin = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.firstOrNull { - val network = currency.network - it.network.id == network.id && it.network.derivationPath == network.derivationPath - } - ?: error("Unable to create network coin for currencyID: ${currency.id}") - } else { - currenciesRepository.getNetworkCoin( - userWalletId = userWalletId, - networkId = currency.network.id, - derivationPath = currency.network.derivationPath, - ) - } + val networkCoin = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.firstOrNull { + val network = currency.network + it.network.id == network.id && it.network.derivationPath == network.derivationPath + } + ?: error("Unable to create network coin for currencyID: ${currency.id}") + if (isBalanceZero(userWalletId, networkCoin)) { raise(AssociateAssetError.NotEnoughBalance(networkCoin)) } + val signer = cardSdkConfigRepository.getCommonSigner( cardId = null, twinKey = null, // use null here because no assets support for Twin cards diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 6f1080a389..9660b61db5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -22,7 +22,10 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase @@ -62,9 +65,7 @@ internal class NFTSendModel @Inject constructor( private val router: Router, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, @@ -178,16 +179,10 @@ internal class NFTSendModel @Inject constructor( ifRight = { wallet -> userWallet = wallet - cryptoCurrency = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - ?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network } - } else { - getCryptoCurrenciesUseCase(userWalletId).getOrNull() - ?.filterIsInstance() - ?.firstOrNull { it.network == nftAsset.network } - } + cryptoCurrency = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network } ?: return@launch getCurrenciesStatusUpdates( @@ -244,7 +239,7 @@ internal class NFTSendModel @Inject constructor( ).getOrNull() ?: cryptoStatus if (uiState.value.destinationUM is DestinationUM.Empty) { - router.replaceAll(CommonSendRoute.Destination(isEditMode = false)) + router.replaceAll(Destination(isEditMode = false)) } }, ifLeft = { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index 575ee282d4..cae468e2b2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -6,14 +6,12 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import dagger.assisted.Assisted @@ -29,9 +27,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( @Assisted private val queryParams: Map, private val appRouter: AppRouter, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val getYieldUseCase: GetYieldUseCase, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, ) : StakingDeepLinkHandler { @@ -56,17 +52,10 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( } scope.launch { - val cryptoCurrency = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId), - ) - .orEmpty() - } else { - getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse { - Timber.e("Error on getting crypto currency list") - return@launch - } - } + val cryptoCurrency = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId), + ) + .orEmpty() .firstOrNull { val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index de44a22010..97a6e45689 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -72,7 +72,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, private val initialToCurrencyResolver: InitialToCurrencyResolver, private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, @@ -1761,14 +1760,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (feePaidCurrency.balance > fee.multiply(percentsToFeeIncrease)) { SwapFeeState.Enough } else { - val tokens = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - } else { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - } + val tokens = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + .orEmpty() val token = tokens .filterIsInstance() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index a88858e751..c8458fac52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -12,13 +12,15 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.notifications.models.NotificationType -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.notifications.models.NotificationType +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents @@ -45,9 +47,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger, private val analyticsEventHandler: AnalyticsEventHandler, private val getUserWalletUseCase: GetUserWalletUseCase, - private val tokensFeatureToggles: TokensFeatureToggles, private val walletBalanceFetcher: WalletBalanceFetcher, - private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, ) : TokenDetailsDeepLinkHandler { init { @@ -119,24 +119,15 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private suspend fun fetchCurrency(userWallet: UserWallet, cryptoCurrency: CryptoCurrency) { val isMultiCurrency = userWallet.isMultiCurrency // single-currency wallet with token (NODL) - val isSingleWalletWithToken = userWallet is UserWallet.Cold && + userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() when { isMultiCurrency -> fetchCurrencyStatusUseCase.invoke( userWalletId = userWallet.walletId, id = cryptoCurrency.id, ) - !isMultiCurrency && tokensFeatureToggles.isWalletBalanceFetcherEnabled -> - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - // remove below after delete tokensFeatureToggles.isWalletBalanceFetcherEnabled - !isMultiCurrency && userWallet is UserWallet.Cold && isSingleWalletWithToken -> - fetchCardTokenListUseCase.invoke( - userWalletId = userWallet.walletId, - refresh = true, - ) - !isMultiCurrency -> fetchCurrencyStatusUseCase.invoke( - userWalletId = userWallet.walletId, - refresh = true, + !isMultiCurrency -> walletBalanceFetcher( + params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index d90595d32f..7e4574513d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,7 +10,6 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -20,9 +19,7 @@ import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUse import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.* -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -76,9 +73,7 @@ internal class WalletModel @Inject constructor( private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val walletContentFetcher: WalletContentFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, private val walletDeepLinkActionListener: WalletDeepLinkActionListener, private val notificationsRepository: NotificationsRepository, @@ -406,13 +401,11 @@ internal class WalletModel @Inject constructor( val otherWallets = action.wallets.minus(action.selectedWallet) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - otherWallets - .filterNot(UserWallet::isLocked) - .onEach { userWallet -> - modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } - } - } + otherWallets + .filterNot(UserWallet::isLocked) + .onEach { userWallet -> + modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { val direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { @@ -570,27 +563,14 @@ internal class WalletModel @Inject constructor( } private suspend fun fetchWalletContent(userWallet: UserWallet) { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - if (userWallet.isLocked) return + if (userWallet.isLocked) return - /* - * Updating the balance of the current wallet is an essential part of InitializationWallets, - * so the coroutine is launched in the current context - */ - supervisorScope { - launch { walletContentFetcher(userWalletId = userWallet.walletId) } - } - } else { - fetchIfSingleWallet(userWallet = userWallet) - } - } - - private fun fetchIfSingleWallet(userWallet: UserWallet) { - if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { - modelScope.launch { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) - .onLeft { Timber.e(it.toString()) } - } + /* + * Updating the balance of the current wallet is an essential part of InitializationWallets, + * so the coroutine is launched in the current context + */ + supervisorScope { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 55fd1893e3..115b1114ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -1,18 +1,10 @@ package com.tangem.feature.wallet.child.wallet.model.intents import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.extenstions.unwrap -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview -import com.tangem.domain.tokens.FetchCardTokenListUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.FetchTokenListUseCase -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -23,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetRefreshStateTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -43,12 +34,7 @@ internal class WalletClickIntents @Inject constructor( private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, - private val fetchTokenListUseCase: FetchTokenListUseCase, private val walletContentFetcher: WalletContentFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, - private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, private val rampStateManager: RampStateManager, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, @@ -88,7 +74,7 @@ internal class WalletClickIntents @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled && !it.isLocked) { + if (!it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } @@ -131,28 +117,7 @@ internal class WalletClickIntents @Inject constructor( ) modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) - } else { - val isSingleWalletWithToken = userWallet is UserWallet.Cold && - userWallet.cardTypesResolver.isSingleWalletWithToken() - - val maybeFetchResult = if (isSingleWalletWithToken) { - fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) - } else { - fetchTokenListUseCase(userWalletId = userWallet.walletId) - } - - maybeFetchResult.onLeft { - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = it, - appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - ), - ) - } - } + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) buildList { async { rampStateManager.fetchSellServiceData() }.let(::add) @@ -177,11 +142,7 @@ internal class WalletClickIntents @Inject constructor( ) modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) - } else { - fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) - } + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load(