From 0c5d018677198aca94e2df901169a6bb4c1d71d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 19:56:51 +0500 Subject: [PATCH 01/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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( From e590fffa8bc65855b9d35d004404f5509225fa07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Sep 2025 09:45:57 +0000 Subject: [PATCH 49/69] 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 5ebc59103a..72c784bae5 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-1214" +tangemBlockchainSdk = "develop-1216" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +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 ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From e323c7cc4a10f2433bbc7a033624103cf9f077f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Sep 2025 16:21:48 +0400 Subject: [PATCH 50/69] Updated on 2026-08-14 --- app/build.gradle.kts | 1 - .../di/domain/NotificationsDomainModule.kt | 9 ------- .../DefaultNotificationsFeatureToggles.kt | 11 -------- .../tangem/tap/features/main/MainViewModel.kt | 18 ++++++------- .../configs/feature_toggles_config.json | 4 --- data/common/build.gradle.kts | 1 - .../UserTokensResponseAddressesEnricher.kt | 6 ----- .../tangem/data/common/di/DataCommonModule.kt | 3 --- ...UserTokensResponseAddressesEnricherTest.kt | 8 ------ domain/notifications/toggles/.gitignore | 1 - domain/notifications/toggles/build.gradle.kts | 4 --- .../toggles/NotificationsFeatureToggles.kt | 5 ---- .../push-notifications/impl/build.gradle.kts | 1 - ...ltPushNotificationsBottomSheetComponent.kt | 4 --- .../impl/DefaultPushNotificationsComponent.kt | 6 ++--- .../impl/model/PushNotificationsModel.kt | 26 ++++--------------- .../presentation/state/PushNotificationsUM.kt | 5 ---- .../ui/PushNotificationsBottomSheet.kt | 19 ++++---------- .../ui/PushNotificationsScreen.kt | 18 ++++--------- .../wallet-settings/impl/build.gradle.kts | 1 - .../model/WalletSettingsModel.kt | 5 +--- features/wallet/impl/build.gradle.kts | 1 - .../wallet/child/wallet/model/WalletModel.kt | 3 --- settings.gradle.kts | 1 - 24 files changed, 27 insertions(+), 134 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt delete mode 100644 domain/notifications/toggles/.gitignore delete mode 100644 domain/notifications/toggles/build.gradle.kts delete mode 100644 domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt delete mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/state/PushNotificationsUM.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 720052668f..a5fdb2e580 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,7 +149,6 @@ dependencies { implementation(projects.domain.quotes) implementation(projects.domain.notifications) implementation(projects.domain.notifications.models) - implementation(projects.domain.notifications.toggles) implementation(projects.domain.swap.models) implementation(projects.domain.swap) implementation(projects.domain.walletManager) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index b35ae23d95..5e28ca4628 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -1,11 +1,8 @@ package com.tangem.tap.di.domain -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.* import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.repository.PushNotificationsRepository -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles -import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.Module import dagger.Provides @@ -79,12 +76,6 @@ internal object NotificationsDomainModule { ) } - @Provides - @Singleton - fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { - return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager) - } - @Provides @Singleton fun provideGetNetworksAvailableForNotifications( diff --git a/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt deleted file mode 100644 index 5a58923bb9..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.domain.notifications - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles - -internal class DefaultNotificationsFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : NotificationsFeatureToggles { - override val isNotificationsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") -} \ No newline at end of file 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 777ff7cc9a..5fba945d69 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 @@ -28,7 +28,6 @@ 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 -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -80,7 +79,6 @@ internal class MainViewModel @Inject constructor( private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, private val onboardingRepository: OnboardingRepository, - private val notificationsToggles: NotificationsFeatureToggles, private val getApplicationIdUseCase: GetApplicationIdUseCase, private val subscribeOnWalletsUseCase: GetSavedWalletsCountUseCase, private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, @@ -415,18 +413,20 @@ internal class MainViewModel @Inject constructor( } private suspend fun initPushNotifications() { - if (notificationsToggles.isNotificationsEnabled) { - getApplicationIdUseCase().onRight { applicationId -> + getApplicationIdUseCase() + .onRight { applicationId -> sendPushTokenUseCase(applicationId = applicationId) associateWalletsWithApplicationId(applicationId = applicationId) updateRemoteWalletsInfoUseCase(applicationId = applicationId) - }.onLeft { Timber.e(it.toString()) } - } + } + .onLeft(Timber::e) } private fun associateWalletsWithApplicationId(applicationId: ApplicationId) { - subscribeOnWalletsUseCase().onEach { wallets -> - associateWalletsWithApplicationIdUseCase(applicationId, wallets) - }.launchIn(viewModelScope) + subscribeOnWalletsUseCase() + .onEach { wallets -> + associateWalletsWithApplicationIdUseCase(applicationId, wallets) + } + .launchIn(viewModelScope) } } \ 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 89dcd4edea..ec0d4d8507 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 @@ -23,10 +23,6 @@ "name": "WALLET_CONNECT_REDESIGN_ENABLED", "version": "5.27.0" }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "version": "5.26.2" - }, { "name": "USEDESK_ENABLED", "version": "undefined" diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index 6402820dd1..ece3e5297a 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - implementation(projects.domain.notifications.toggles) implementation(projects.domain.networks) implementation(projects.domain.wallets) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt index 96f45165c5..2b155a84fc 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt @@ -5,7 +5,6 @@ import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.first @@ -15,17 +14,12 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds class UserTokensResponseAddressesEnricher @Inject constructor( - private val notificationsFeatureToggles: NotificationsFeatureToggles, private val walletsRepository: WalletsRepository, private val dispatchers: CoroutineDispatcherProvider, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, ) { suspend operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse { - if (!notificationsFeatureToggles.isNotificationsEnabled) { - return response - } - val isNotificationsEnabled = walletsRepository.isNotificationsEnabled(userWalletId) return withContext(dispatchers.default) { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index 2e6266a009..85260e75da 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -42,13 +41,11 @@ internal object DataCommonModule { @Provides @Singleton fun provideUserTokensEncricher( - notificationsFeatureToggles: NotificationsFeatureToggles, walletsRepository: WalletsRepository, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, dispatchers: CoroutineDispatcherProvider, ): UserTokensResponseAddressesEnricher { return UserTokensResponseAddressesEnricher( - notificationsFeatureToggles = notificationsFeatureToggles, walletsRepository = walletsRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, dispatchers = dispatchers, diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt index faf985a7b2..aed68d658d 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt @@ -7,7 +7,6 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -23,7 +22,6 @@ import org.junit.Test class UserTokensResponseAddressesEnricherTest { - private lateinit var notificationsFeatureToggles: NotificationsFeatureToggles private lateinit var walletsRepository: WalletsRepository private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier @@ -31,12 +29,10 @@ class UserTokensResponseAddressesEnricherTest { @Before fun setup() { - notificationsFeatureToggles = mockk() walletsRepository = mockk() multiNetworkStatusSupplier = mockk() enricher = UserTokensResponseAddressesEnricher( - notificationsFeatureToggles = notificationsFeatureToggles, walletsRepository = walletsRepository, dispatchers = dispatchers, multiNetworkStatusSupplier = multiNetworkStatusSupplier, @@ -54,7 +50,6 @@ class UserTokensResponseAddressesEnricherTest { val userWalletId = UserWalletId("1234567890abcdef") val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) - every { notificationsFeatureToggles.isNotificationsEnabled } returns false // WHEN val result = enricher(userWalletId, response) @@ -70,7 +65,6 @@ class UserTokensResponseAddressesEnricherTest { val userWalletId = UserWalletId("1234567890abcdef") val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) - every { notificationsFeatureToggles.isNotificationsEnabled } returns true coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns false coEvery { multiNetworkStatusSupplier.invoke(any()) @@ -110,7 +104,6 @@ class UserTokensResponseAddressesEnricherTest { val response = createUserTokensResponse(tokens = listOf(token)) val addresses = listOf("0x123", "0x456") - every { notificationsFeatureToggles.isNotificationsEnabled } returns true coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true coEvery { multiNetworkStatusSupplier.invoke(any()) @@ -152,7 +145,6 @@ class UserTokensResponseAddressesEnricherTest { val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) - every { notificationsFeatureToggles.isNotificationsEnabled } returns true coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true coEvery { multiNetworkStatusSupplier.invoke(any()) diff --git a/domain/notifications/toggles/.gitignore b/domain/notifications/toggles/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/domain/notifications/toggles/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/domain/notifications/toggles/build.gradle.kts b/domain/notifications/toggles/build.gradle.kts deleted file mode 100644 index 7ff7fb7522..0000000000 --- a/domain/notifications/toggles/build.gradle.kts +++ /dev/null @@ -1,4 +0,0 @@ -plugins { - alias(deps.plugins.kotlin.jvm) - id("configuration") -} \ No newline at end of file diff --git a/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt b/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt deleted file mode 100644 index 22bbec7d10..0000000000 --- a/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.domain.notifications.toggles - -interface NotificationsFeatureToggles { - val isNotificationsEnabled: Boolean -} \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index 2f2c875648..e8a673832e 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -40,7 +40,6 @@ dependencies { /** Domain module */ implementation(projects.domain.settings) - implementation(projects.domain.notifications.toggles) implementation(projects.domain.notifications) /** Feature modules */ diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsBottomSheetComponent.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsBottomSheetComponent.kt index 259b0bacf0..2862913418 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsBottomSheetComponent.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsBottomSheetComponent.kt @@ -1,8 +1,6 @@ package com.tangem.features.pushnotifications.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -38,7 +36,6 @@ internal class DefaultPushNotificationsBottomSheetComponent @AssistedInject cons @Composable override fun BottomSheet() { - val state by model.state.collectAsState() val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( isShown = true, @@ -55,7 +52,6 @@ internal class DefaultPushNotificationsBottomSheetComponent @AssistedInject cons onLaterClick = model::onLaterClick, onAllowPermission = model::onAllowPermission, onDenyPermission = model::onDenyPermission, - showNotificationsInfo = state.showInfoAboutNotifications, ) } } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt index 458d977d4e..3eb2a6fdc9 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt @@ -2,8 +2,6 @@ package com.tangem.features.pushnotifications.impl import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.tangem.core.decompose.context.AppComponentContext @@ -28,15 +26,15 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val activity = LocalContext.current.findActivity() - val state by model.state.collectAsState() + BackHandler(onBack = { activity.finish() }) NavigationBar3ButtonsScrim() + PushNotificationsScreen( onAllowClick = model::onAllowClick, onLaterClick = model::onLaterClick, onAllowPermission = model::onAllowPermission, onDenyPermission = model::onDenyPermission, - showNotificationsInfo = state.showInfoAboutNotifications, ) } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index c76c250f14..bd3edb7000 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -9,16 +9,12 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.features.pushnotifications.api.PushNotificationsParams import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION -import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,7 +28,6 @@ internal class PushNotificationsModel @Inject constructor( private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val appRouter: AppRouter, private val analyticHandler: AnalyticsEventHandler, - private val notificationsFeatureToggles: NotificationsFeatureToggles, private val notificationsRepository: NotificationsRepository, ) : Model(), PushNotificationsClickIntents { @@ -43,32 +38,21 @@ internal class PushNotificationsModel @Inject constructor( AppRoute.PushNotification.Source.Onboarding -> AnalyticsParam.ScreensSources.Onboarding } - private val _state = MutableStateFlow( - PushNotificationsUM( - showInfoAboutNotifications = notificationsFeatureToggles.isNotificationsEnabled, - ), - ) - init { analyticHandler.send(PushNotificationAnalyticEvents.NotificationsScreenOpened(source)) } - val state = _state.asStateFlow() - override fun onAllowClick() { - if (notificationsFeatureToggles.isNotificationsEnabled) { - modelScope.launch { - notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true) - } + modelScope.launch { + notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true) } + analyticHandler.send(PushNotificationAnalyticEvents.ButtonAllow(source)) } override fun onLaterClick() { - if (notificationsFeatureToggles.isNotificationsEnabled) { - modelScope.launch { - notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false) - } + modelScope.launch { + notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false) } analyticHandler.send(PushNotificationAnalyticEvents.ButtonLater(source)) modelScope.launch { diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/state/PushNotificationsUM.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/state/PushNotificationsUM.kt deleted file mode 100644 index a7161b630b..0000000000 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/state/PushNotificationsUM.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.pushnotifications.impl.presentation.state - -data class PushNotificationsUM( - val showInfoAboutNotifications: Boolean = false, -) \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsBottomSheet.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsBottomSheet.kt index fa78628411..07090215d9 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsBottomSheet.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsBottomSheet.kt @@ -44,7 +44,6 @@ internal fun PushNotificationsContent( onLaterClick: () -> Unit, onAllowPermission: () -> Unit, onDenyPermission: () -> Unit, - showNotificationsInfo: Boolean, ) { val requestPushPermission = requestPermission( onAllow = onAllowPermission, @@ -65,18 +64,11 @@ internal fun PushNotificationsContent( R.drawable.ic_storefront_24, resourceReference(R.string.user_push_notification_agreement_argument_two), ), - ).let { baseItems -> - if (showNotificationsInfo) { - baseItems.add( - ShowcaseItemModel( - R.drawable.ic_notifications_24, - resourceReference(R.string.user_push_notification_agreement_argument_three), - ), - ) - } else { - baseItems - } - }, + ShowcaseItemModel( + R.drawable.ic_notifications_24, + resourceReference(R.string.user_push_notification_agreement_argument_three), + ), + ), modifier = Modifier.padding(top = TangemTheme.dimens.spacing40), ) SpacerH28() @@ -111,7 +103,6 @@ private fun Preview_PushNotificationsBottomSheet() { onLaterClick = {}, onAllowPermission = {}, onDenyPermission = {}, - showNotificationsInfo = true, ) } } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt index 4750463f50..81ec2b2743 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -18,7 +18,6 @@ internal fun PushNotificationsScreen( onLaterClick: () -> Unit, onAllowPermission: () -> Unit, onDenyPermission: () -> Unit, - showNotificationsInfo: Boolean, ) { val requestPushPermission = requestPermission( onAllow = onAllowPermission, @@ -38,18 +37,11 @@ internal fun PushNotificationsScreen( R.drawable.ic_storefront_24, resourceReference(R.string.user_push_notification_agreement_argument_two), ), - ).let { baseItems -> - if (showNotificationsInfo) { - baseItems.add( - ShowcaseItemModel( - R.drawable.ic_notifications_24, - resourceReference(R.string.user_push_notification_agreement_argument_three), - ), - ) - } else { - baseItems - } - }, + ShowcaseItemModel( + R.drawable.ic_notifications_24, + resourceReference(R.string.user_push_notification_agreement_argument_three), + ), + ), primaryButton = ShowcaseButtonModel( buttonText = resourceReference(R.string.common_allow), onClick = { diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 4ab6bc0a40..3bb8904189 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -41,7 +41,6 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.nft) implementation(projects.domain.settings) - implementation(projects.domain.notifications.toggles) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications) 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 5eba834c7e..fac9ee92d6 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 @@ -32,7 +32,6 @@ import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase 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.wallets.usecase.* import com.tangem.feature.walletsettings.analytics.Settings @@ -73,7 +72,6 @@ internal class WalletSettingsModel @Inject constructor( getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, private val enableWalletNFTUseCase: EnableWalletNFTUseCase, private val disableWalletNFTUseCase: DisableWalletNFTUseCase, - private val notificationsToggles: NotificationsFeatureToggles, getWalletNotificationsEnabledUseCase: GetWalletNotificationsEnabledUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val settingsManager: SettingsManager, @@ -130,8 +128,7 @@ internal class WalletSettingsModel @Inject constructor( is UserWallet.Hot -> wallet.backedUp is UserWallet.Cold -> true } - val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && - !getIsHuaweiDeviceWithoutGoogleServicesUseCase() + val isNeedShowNotifications = !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> value.copy( items = buildItems( diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 1df9de0f95..daafa8122d 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -95,7 +95,6 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) - implementation(projects.domain.notifications.toggles) implementation(projects.domain.transaction) /** Feature Apis */ 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 7e4574513d..b72ac8cf5c 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 @@ -17,7 +17,6 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.usecase.* @@ -79,7 +78,6 @@ internal class WalletModel @Inject constructor( private val notificationsRepository: NotificationsRepository, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, - private val notificationsFeatureToggles: NotificationsFeatureToggles, private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -575,7 +573,6 @@ internal class WalletModel @Inject constructor( } private fun enableNotificationsIfNeeded() { - if (!notificationsFeatureToggles.isNotificationsEnabled) return modelScope.launch { val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications() if (isUserAllowToEnableNotifications) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 1511ce8a21..19c6d1c875 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -341,7 +341,6 @@ include(":domain:blockaid") include(":domain:blockaid:models") include(":domain:notifications") include(":domain:notifications:models") -include(":domain:notifications:toggles") include(":domain:express") include(":domain:express:models") include(":domain:swap") From 2471b9c1a8fb8227c3226b88a87b5c080ff5d9c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Sep 2025 18:33:57 +0400 Subject: [PATCH 51/69] Updated on 2026-08-14 --- data/common/build.gradle.kts | 2 + .../UserTokensResponseAccountIdEnricher.kt | 94 +++++++++++ .../data/common/currency/UserTokensSaver.kt | 93 +++++++---- .../tangem/data/common/di/DataCommonModule.kt | 7 +- ...UserTokensResponseAccountIdEnricherTest.kt | 151 ++++++++++++++++++ .../common/currency/UserTokensSaverTest.kt | 7 +- 6 files changed, 317 insertions(+), 37 deletions(-) create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt create mode 100644 data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricherTest.kt diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index ece3e5297a..9994bc2f46 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.utils) /* Domain */ + implementation(projects.domain.account) implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.card) @@ -31,6 +32,7 @@ dependencies { /* Libs - SDK */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(projects.libs.crypto) implementation(projects.libs.blockchainSdk) /* DI */ diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt new file mode 100644 index 0000000000..b52d3c3f86 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt @@ -0,0 +1,94 @@ +package com.tangem.data.common.currency + +import arrow.core.getOrElse +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer +import timber.log.Timber + +/** + * Enriches the [UserTokensResponse] with accountId values for tokens + * +[REDACTED_AUTHOR] + */ +object UserTokensResponseAccountIdEnricher { + + /** + * Enriches the tokens in the given [UserTokensResponse] with accountId values + * + * @param userWalletId the ID of the user wallet + * @param response the [UserTokensResponse] containing tokens to be enriched + */ + operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse { + val hasUnassignedTokens = response.tokens.any { it.accountId == null } + if (!hasUnassignedTokens) return response + + val enrichedTokens = response.tokens + .filter { it.accountId == null } + .groupByAccountIndex() + .mapKeysToAccountId(userWalletId) + .mapToEnrichedTokens() + + if (enrichedTokens.isEmpty()) return response + + return response.copy( + tokens = response.tokens.map { token -> + val enrichedToken = enrichedTokens.find { it == token } + enrichedToken ?: token + }, + ) + } + + private fun List.groupByAccountIndex(): Map> { + return this + .groupBy { savedToken -> + val derivationPathValue = savedToken.derivationPath + if (derivationPathValue == null) { + Timber.e("Token $savedToken has no derivation path") + return@groupBy null + } + + val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId) + if (blockchain == null) { + Timber.e("Token $savedToken has unknown networkId") + return@groupBy null + } + + val accountNodeRecognizer = AccountNodeRecognizer(blockchain) + val accountIndex = accountNodeRecognizer.recognize(derivationPathValue) + if (accountIndex == null) { + Timber.e("Token $savedToken has unrecognized derivation path") + return@groupBy null + } + + accountIndex + } + } + + private fun Map>.mapKeysToAccountId( + userWalletId: UserWalletId, + ): Map> { + return mapKeys { (accountIndex, _) -> + if (accountIndex == null) return@mapKeys null + + val derivationIndex = DerivationIndex.invoke(value = accountIndex.toInt()).getOrElse { + Timber.e("Failed to parse derivation index from account index: $accountIndex") + return@mapKeys null + } + + AccountId.forCryptoPortfolio(userWalletId, derivationIndex) + } + } + + private fun Map>.mapToEnrichedTokens(): List { + return flatMap { (accountId, tokens) -> + if (accountId == null) return@flatMap emptyList() + + tokens.map { it.copy(accountId = accountId.value) } + } + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 95c9010707..e0bfb846d8 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -5,60 +5,85 @@ import com.tangem.data.common.tokens.UserTokensBackwardCompatibility import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext -import timber.log.Timber class UserTokensSaver( private val tangemTechApi: TangemTechApi, private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, - private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher, + private val addressesEnricher: UserTokensResponseAddressesEnricher, + private val accountsFeatureToggles: AccountsFeatureToggles, ) { private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() - suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) = - withContext(dispatchers.io) { - val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) - val enrichedUserTokensResponse = if (useEnricher) { - userTokensResponseAddressesEnricher( - userWalletId = userWalletId, - response = compatibleUserTokensResponse, - ) - } else { - compatibleUserTokensResponse - } - - userTokensResponseStore.store(userWalletId = userWalletId, response = enrichedUserTokensResponse) - } - suspend fun storeAndPush(userWalletId: UserWalletId, response: UserTokensResponse) { - val enrichedUserTokensResponse = userTokensResponseAddressesEnricher( - userWalletId = userWalletId, - response = response, - ) - store(userWalletId, enrichedUserTokensResponse, false) - push(userWalletId, enrichedUserTokensResponse, false) + withContext(dispatchers.default) { + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = true) + + store(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false) + push(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false) + } } + suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) = + withContext(dispatchers.default) { + val updatedResponse = response + .applyCompatibility() + .enrichIf(userWalletId = userWalletId, condition = useEnricher) + + userTokensResponseStore.store(userWalletId = userWalletId, response = updatedResponse) + } + suspend fun push( userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true, onFailSend: () -> Unit = {}, - ) = withContext(dispatchers.io) { - val enrichedUserTokensResponse = if (useEnricher) { - userTokensResponseAddressesEnricher( - userWalletId = userWalletId, - response = response, + ) { + withContext(dispatchers.default) { + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + + safeApiCall( + call = { + withContext(dispatchers.io) { + tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse) + .bind() + } + }, + onError = { onFailSend() }, ) - } else { - response - } - safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedUserTokensResponse).bind() }) { - Timber.e(it, "Unable to push user tokens for: ${userWalletId.stringValue}") - onFailSend() } } + + private fun UserTokensResponse.applyCompatibility(): UserTokensResponse { + return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this) + } + + private suspend fun UserTokensResponse.enrichIf( + userWalletId: UserWalletId, + condition: Boolean, + ): UserTokensResponse { + if (!condition) return this + + return this + .enrichByAddress(userWalletId = userWalletId) + .let { + if (accountsFeatureToggles.isFeatureEnabled) { + it.enrichByAccountId(userWalletId = userWalletId) + } else { + it + } + } + } + + private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse { + return addressesEnricher(userWalletId = userWalletId, response = this) + } + + private fun UserTokensResponse.enrichByAccountId(userWalletId: UserWalletId): UserTokensResponse { + return UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, response = this) + } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index 85260e75da..9b18e1b8fd 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -7,6 +7,7 @@ import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.wallets.repository.WalletsRepository @@ -58,13 +59,15 @@ internal object DataCommonModule { tangemTechApi: TangemTechApi, userTokensResponseStore: UserTokensResponseStore, dispatchers: CoroutineDispatcherProvider, - enricher: UserTokensResponseAddressesEnricher, + addressesEnricher: UserTokensResponseAddressesEnricher, + accountsFeatureToggles: AccountsFeatureToggles, ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, - userTokensResponseAddressesEnricher = enricher, + addressesEnricher = addressesEnricher, + accountsFeatureToggles = accountsFeatureToggles, ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricherTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricherTest.kt new file mode 100644 index 0000000000..bd7c253d03 --- /dev/null +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricherTest.kt @@ -0,0 +1,151 @@ +package com.tangem.data.common.currency + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UserTokensResponseAccountIdEnricherTest { + + private val userWalletId = UserWalletId("011") + private val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + + @Test + fun `enriches tokens with missing account ids`() { + // Arrange + val response = mockCryptoCurrencyFactory.ethereumAndStellar + .mapIndexed { index, currency -> + currency.toResponseToken( + accountId = null, + derivationPath = "m/44'/60'/$index'/0/0", + ) + } + .toResponse() + + // Act + val actual = UserTokensResponseAccountIdEnricher(userWalletId, response) + + // Assert + val expected = response.tokens + .mapIndexed { index, currency -> + currency.enrichWithAccountId(accountIndex = index) + } + .toResponse() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `does not modify tokens with existing account ids`() { + // Arrange + val response = mockCryptoCurrencyFactory.ethereumAndStellar + .mapIndexed { index, currency -> + currency.toResponseToken(derivationPath = "m/44'/60'/$index'/0/0") + .enrichWithAccountId(accountIndex = index) + } + .toResponse() + + // Act + val actual = UserTokensResponseAccountIdEnricher(userWalletId, response) + + // Assert + val expected = response + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `skips tokens with invalid derivation paths`() { + // Arrange + val validDerivationPath = "m/44'/60'/0'/0/0" + val invalidDerivationPath = "invalid/path" + + val tokenWithInvalidPath = mockCryptoCurrencyFactory.ethereum.toResponseToken( + accountId = null, + derivationPath = invalidDerivationPath, + ) + + val tokenWithValidPath = mockCryptoCurrencyFactory.stellar.toResponseToken( + accountId = null, + derivationPath = validDerivationPath, + ) + + val response = listOf(tokenWithInvalidPath, tokenWithValidPath).toResponse() + + // Act + val actual = UserTokensResponseAccountIdEnricher(userWalletId, response) + + // Assert + val expected = listOf( + tokenWithInvalidPath, + tokenWithValidPath.enrichWithAccountId(accountIndex = 0), + ).toResponse() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `skips tokens with unknown network id`() { + // Arrange + val unknownNetworkId = "unknown" + val validNetworkId = mockCryptoCurrencyFactory.ethereum.network.rawId + + val tokenWithUnknownNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken( + networkId = unknownNetworkId, + derivationPath = "m/44'/60'/0'/0/0", + accountId = null, + ) + + val tokenWithValidNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken( + accountId = null, + networkId = validNetworkId, + derivationPath = "m/44'/60'/0'/0/0", + ) + + val response = listOf(tokenWithUnknownNetworkId, tokenWithValidNetworkId).toResponse() + + // Act + val actual = UserTokensResponseAccountIdEnricher(userWalletId, response) + + // Assert + val expected = listOf( + tokenWithUnknownNetworkId, + tokenWithValidNetworkId.enrichWithAccountId(accountIndex = 0), + ).toResponse() + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun CryptoCurrency.toResponseToken( + accountId: AccountId? = null, + networkId: String? = null, + derivationPath: String, + ): UserTokensResponse.Token { + return userTokensResponseFactory.createResponseToken(this).copy( + networkId = networkId ?: network.rawId, + derivationPath = derivationPath, + accountId = accountId?.value, + ) + } + + private fun List.toResponse(): UserTokensResponse { + return UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = this, + ) + } + + private fun UserTokensResponse.Token.enrichWithAccountId(accountIndex: Int): UserTokensResponse.Token { + val derivationIndex = DerivationIndex(value = accountIndex).getOrNull()!! + val accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex) + + return copy(accountId = accountId.value) + } +} \ No newline at end of file diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 6b4a086180..66e4d28c01 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -19,12 +20,16 @@ class UserTokensSaverTest { private val tangemTechApi: TangemTechApi = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) private val enricher: UserTokensResponseAddressesEnricher = mockk() + private val accountsFeatureToggles = mockk { + every { this@mockk.isFeatureEnabled } returns true + } private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, userTokensResponseStore = userTokensResponseStore, - userTokensResponseAddressesEnricher = enricher, dispatchers = TestingCoroutineDispatcherProvider(), + addressesEnricher = enricher, + accountsFeatureToggles = accountsFeatureToggles, ) @BeforeEach From 941863f79bedac4ebfa5f1a1bdc7733bde41d219 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Sep 2025 17:10:01 +0500 Subject: [PATCH 52/69] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 7 +- .../tangem/tap/routing/utils/ChildFactory.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 2 +- .../transactions/TxHistoryGroupTitle.kt | 2 +- .../tangempay/details/api/build.gradle.kts | 3 + .../components/TangemPayDetailsComponent.kt | 4 +- .../tangempay/details/impl/build.gradle.kts | 6 + .../DefaultTangemPayDetailsComponent.kt | 8 +- .../DefaultTangemPayTxHistoryComponent.kt | 26 +++ .../PreviewTangemPayTxHistoryComponent.kt | 174 ++++++++++++++++++ .../txHistory/TangemPayTxHistoryComponent.kt | 11 ++ .../tangempay/di/TangemPayModelModule.kt | 8 +- .../model/TangemPayTxHistoryModel.kt | 62 +++++++ .../tangempay/ui/TangemPayDetailsScreen.kt | 24 ++- .../features/txhistory/entity/TxHistoryUM.kt | 5 +- .../features/txhistory/ui/TxHistoryContent.kt | 31 +--- .../component/DefaultTxHistoryComponent.kt | 1 - .../extension/BaseExtensionConfigurations.kt | 1 + 18 files changed, 334 insertions(+), 43 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt rename features/txhistory/{impl => api}/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt (83%) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9cb6b6cd62..659aaec327 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -525,8 +525,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { // Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs if (tangemPayFeatureToggles.isTangemPayEnabled) { - store.dispatchNavigationAction { - replaceAll(AppRoute.TangemPayDetails) + lifecycleScope.launch { + val selectedUserWalledId = userWalletsListRepository.selectedUserWalletSync()?.walletId + store.dispatchNavigationAction { + replaceAll(AppRoute.TangemPayDetails(requireNotNull(selectedUserWalledId))) + } } } else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { 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 708e4bc745..5276185422 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 @@ -591,7 +591,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.TangemPayDetails -> { createComponentChild( context = context, - params = TangemPayDetailsComponent.Params(), + params = TangemPayDetailsComponent.Params(userWalletId = route.userWalletId), componentFactory = tangemPayDetailsComponentFactory, ) } 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 d2204b5cd1..7e98053f5f 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 @@ -369,5 +369,5 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") @Serializable - data object TangemPayDetails : AppRoute(path = "/tangem_pay_details") + data class TangemPayDetails(val userWalletId: UserWalletId) : AppRoute(path = "/tangem_pay_details") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt index e0298ceaed..a34ed1e6c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt @@ -23,7 +23,7 @@ import java.util.UUID * @param modifier modifier */ @Composable -internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) { +fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) { Box( modifier = modifier .background(TangemTheme.colors.background.primary) diff --git a/features/tangempay/details/api/build.gradle.kts b/features/tangempay/details/api/build.gradle.kts index 77acdd5c22..7d64c9e77f 100644 --- a/features/tangempay/details/api/build.gradle.kts +++ b/features/tangempay/details/api/build.gradle.kts @@ -13,6 +13,9 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) + /** Domain */ + implementation(projects.domain.models) + /** Compose */ implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 64e11cecbe..bd9326f7e9 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -2,9 +2,9 @@ package com.tangem.features.tangempay.components import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId interface TangemPayDetailsComponent : ComposableContentComponent { - @Suppress("EmptyDefaultConstructor") // Will add params in Next PRs - class Params() + data class Params(val userWalletId: UserWalletId) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts index ae6c79993c..84f577edf1 100644 --- a/features/tangempay/details/impl/build.gradle.kts +++ b/features/tangempay/details/impl/build.gradle.kts @@ -19,6 +19,12 @@ dependencies { /** Features api */ implementation(projects.features.tangempay.details.api) + implementation(projects.features.txhistory.api) + + /** Domain */ + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.models) /** Compose */ implementation(deps.compose.foundation) 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 3fe44cd539..cd5e1f5b79 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 @@ -5,8 +5,10 @@ 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.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent import com.tangem.features.tangempay.model.TangemPayDetailsModel import com.tangem.features.tangempay.ui.TangemPayDetailsScreen import dagger.assisted.Assisted @@ -19,12 +21,16 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( ) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { private val model: TangemPayDetailsModel = getOrCreateModel(params = params) + private val txHistoryComponent = DefaultTangemPayTxHistoryComponent( + appComponentContext = child("txHistoryComponent"), + params = DefaultTangemPayTxHistoryComponent.Params(userWalletId = params.userWalletId), + ) @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() NavigationBar3ButtonsScrim() - TangemPayDetailsScreen(state = state, modifier = modifier) + TangemPayDetailsScreen(state = state, txHistoryComponent = txHistoryComponent, modifier = modifier) } @AssistedFactory diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt new file mode 100644 index 0000000000..c0ccc0a291 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -0,0 +1,26 @@ +package com.tangem.features.tangempay.components.txHistory + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.model.TangemPayTxHistoryModel +import com.tangem.features.txhistory.entity.TxHistoryUM +import com.tangem.features.txhistory.ui.txHistoryItems +import kotlinx.coroutines.flow.StateFlow + +internal class DefaultTangemPayTxHistoryComponent( + appComponentContext: AppComponentContext, + params: Params, +) : AppComponentContext by appComponentContext, TangemPayTxHistoryComponent { + + private val model: TangemPayTxHistoryModel = getOrCreateModel(params = params) + override val state: StateFlow = model.uiState + + override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) { + txHistoryItems(listState, state) + } + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt new file mode 100644 index 0000000000..61d078d853 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt @@ -0,0 +1,174 @@ +package com.tangem.features.tangempay.components.txHistory + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.txhistory.entity.TxHistoryUM +import com.tangem.features.txhistory.ui.txHistoryItems +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TxHistoryUM) : TangemPayTxHistoryComponent { + override val state: StateFlow = MutableStateFlow(txHistoryUM) + + override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) { + txHistoryItems(listState, state) + } + + companion object { + val loadingUM = TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {}) + val emptyUM = TxHistoryUM.Empty(isBalanceHidden = true, onExploreClick = {}) + val contentUM = TxHistoryUM.Content( + isBalanceHidden = false, + loadMore = { false }, + items = persistentListOf( + TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = {}), + TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Today", itemKey = "Today"), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-4.99 USD", + time = "16:41", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Starbucks"), + subtitle = stringReference("Food&Drinks"), + timestamp = 3464, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-126.20 USD", + time = "12:04", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Wallmart"), + subtitle = stringReference("Supermarket"), + timestamp = 3465, + ), + ), + TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Yesterday", itemKey = "Yesterday"), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-4.99 USD", + time = "21:41", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Starbucks"), + subtitle = stringReference("Food&Drinks"), + timestamp = 3464, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-126.20 USD", + time = "10:04", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Wallmart"), + subtitle = stringReference("Supermarket"), + timestamp = 3465, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-4.99 USD", + time = "19:41", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Starbucks"), + subtitle = stringReference("Food&Drinks"), + timestamp = 3464, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-126.20 USD", + time = "18:04", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Wallmart"), + subtitle = stringReference("Supermarket"), + timestamp = 3465, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-4.99 USD", + time = "17:41", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Starbucks"), + subtitle = stringReference("Food&Drinks"), + timestamp = 3464, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-126.20 USD", + time = "16:04", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Wallmart"), + subtitle = stringReference("Supermarket"), + timestamp = 3465, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-4.99 USD", + time = "15:41", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Starbucks"), + subtitle = stringReference("Food&Drinks"), + timestamp = 3464, + ), + ), + TxHistoryUM.TxHistoryItemUM.Transaction( + state = TransactionState.Content( + txHash = "signiferumque", + amount = "-126.20 USD", + time = "14:04", + status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, + iconRes = R.drawable.ic_arrow_up_24, + title = stringReference("Wallmart"), + subtitle = stringReference("Supermarket"), + timestamp = 3465, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt new file mode 100644 index 0000000000..3b538ef7fd --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.tangempay.components.txHistory + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.features.txhistory.entity.TxHistoryUM +import kotlinx.coroutines.flow.StateFlow + +internal interface TangemPayTxHistoryComponent { + val state: StateFlow + fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) +} \ No newline at end of file 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 index a9bee0452a..ef72f17c45 100644 --- 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 @@ -3,6 +3,7 @@ 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 com.tangem.features.tangempay.model.TangemPayTxHistoryModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,5 +17,10 @@ internal interface TangemPayModelModule { @Binds @IntoMap @ClassKey(TangemPayDetailsModel::class) - fun bindWcConnectionsModel(model: TangemPayDetailsModel): Model + fun bindTangemPayDetailsModel(model: TangemPayDetailsModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayTxHistoryModel::class) + fun bindTangemPayTxHistoryModel(model: TangemPayTxHistoryModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt new file mode 100644 index 0000000000..e8532b5dbe --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -0,0 +1,62 @@ +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.model.ParamsContainer +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent +import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent +import com.tangem.features.txhistory.entity.TxHistoryUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayTxHistoryModel @Inject constructor( + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + init { + handleBalanceHiding() + subscribeToUiItemChanges() + } + + @Suppress("MagicNumber") + private fun subscribeToUiItemChanges() { + modelScope.launch { + Timber.d("subscribeToUiItemChanges: ${params.userWalletId}") + delay(2000) + uiState.update { PreviewTangemPayTxHistoryComponent.contentUM } + } + } + + private fun handleBalanceHiding() { + getBalanceHidingSettingsUseCase() + .onEach { uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } } + .launchIn(modelScope) + } + + private fun onExploreClick() { + Timber.d("onExploreClick: open explorer") + } + + private fun getInitialState(): TxHistoryUM { + return TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::onExploreClick) + } +} \ 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 index 3318cbe1b5..f68617ebd5 100644 --- 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 @@ -19,11 +19,13 @@ 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.Devices 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 androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig @@ -37,6 +39,8 @@ 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.components.txHistory.PreviewTangemPayTxHistoryComponent +import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState @@ -46,7 +50,11 @@ import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf @Composable -internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifier = Modifier) { +internal fun TangemPayDetailsScreen( + state: TangemPayDetailsUM, + txHistoryComponent: TangemPayTxHistoryComponent, + modifier: Modifier = Modifier, +) { Scaffold( modifier = modifier, topBar = { TangemPayDetailsTopAppBar(config = state.topBarConfig) }, @@ -55,6 +63,7 @@ internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifie ) { scaffoldPaddings -> val listState = rememberLazyListState() val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val txHistoryState by txHistoryComponent.state.collectAsStateWithLifecycle() TangemPullToRefreshContainer( config = state.pullToRefreshConfig, @@ -91,6 +100,8 @@ internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifie ) }, ) + + with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) } } } } @@ -317,14 +328,19 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi ) } -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(device = Devices.PIXEL_7_PRO, group = "day") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO, group = "night") @Composable private fun TangemPayDetailsScreenPreview( @PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM, ) { TangemThemePreview { - TangemPayDetailsScreen(state = state) + TangemPayDetailsScreen( + state = state, + txHistoryComponent = PreviewTangemPayTxHistoryComponent( + txHistoryUM = PreviewTangemPayTxHistoryComponent.contentUM, + ), + ) } } diff --git a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryUM.kt b/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryUM.kt index 9a9960fc8e..ad33da1dc0 100644 --- a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryUM.kt +++ b/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.txhistory.entity import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -81,7 +82,9 @@ sealed interface TxHistoryUM { data class GroupTitle( val title: String, val itemKey: String, - ) : TxHistoryItemUM + ) : TxHistoryItemUM { + val legacyGroupTitle = TxHistoryState.TxHistoryItemState.GroupTitle(title = title, itemKey = itemKey) + } /** * Transaction item diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt b/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt similarity index 83% rename from features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt rename to features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt index 9525d0ede2..aafd528437 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt +++ b/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryContent.kt @@ -1,20 +1,16 @@ package com.tangem.features.txhistory.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.transactions.PendingTxsBlock import com.tangem.core.ui.components.transactions.Transaction +import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle import com.tangem.core.ui.components.transactions.TxHistoryTitle import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState @@ -25,7 +21,7 @@ import com.tangem.features.txhistory.entity.TxHistoryUM private const val LOAD_ITEMS_BUFFER = 20 -internal fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) { +fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) { when (state) { is TxHistoryUM.Content -> contentItems(listState, state) is TxHistoryUM.Empty -> nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick)) @@ -127,7 +123,7 @@ internal fun TxHistoryListItem( ) { when (state) { is TxHistoryUM.TxHistoryItemUM.GroupTitle -> { - TxHistoryGroupTitle(config = state, modifier = modifier) + TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier) } is TxHistoryUM.TxHistoryItemUM.Title -> { TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier) @@ -140,25 +136,4 @@ internal fun TxHistoryListItem( ) } } -} - -@Composable -private fun TxHistoryGroupTitle(config: TxHistoryUM.TxHistoryItemUM.GroupTitle, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .background(TangemTheme.colors.background.primary) - .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing12, - ) - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size24), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = config.title, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - } } \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/component/DefaultTxHistoryComponent.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/component/DefaultTxHistoryComponent.kt index 7ec4d11055..907f834388 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/component/DefaultTxHistoryComponent.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/component/DefaultTxHistoryComponent.kt @@ -2,7 +2,6 @@ package com.tangem.features.txhistory.component import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.* import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.txhistory.entity.TxHistoryUM diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index c19607fae8..7011f2d423 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -29,6 +29,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) } From 411a5915092c46ce467b73c9ccb12b1c0fac91cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Sep 2025 23:22:34 +0500 Subject: [PATCH 53/69] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 31 +++++- .../tap/features/hot/TangemHotSDKProxy.kt | 7 ++ .../tangem/tap/routing/utils/ChildFactory.kt | 11 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + .../wallets/hot/DefaultHotWalletAccessor.kt | 29 +++++ .../domain/wallets/hot/HotWalletAccessor.kt | 6 + .../ClearHotWalletContextualUnlockUseCase.kt | 17 +++ .../usecase/ExportSeedPhraseUseCase.kt | 17 +++ .../UnlockHotWalletContextualUseCase.kt | 18 +++ .../ui/CreateWalletSelectionContent.kt | 2 +- .../features/hotwallet/ViewPhraseComponent.kt | 14 +++ .../viewphrase/DefaultViewPhraseComponent.kt | 38 +++++++ .../viewphrase/di/ViewPhraseModule.kt | 25 +++++ .../viewphrase/entity/ViewPhraseUM.kt | 10 ++ .../viewphrase/model/ViewPhraseModel.kt | 76 +++++++++++++ .../viewphrase/ui/ViewPhraseContent.kt | 103 ++++++++++++++++++ .../walletbackup/model/WalletBackupModel.kt | 85 ++++++++------- gradle/tangem_dependencies.toml | 2 +- 18 files changed, 453 insertions(+), 43 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearHotWalletContextualUnlockUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ExportSeedPhraseUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockHotWalletContextualUseCase.kt create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/ViewPhraseComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/DefaultViewPhraseComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/di/ViewPhraseModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/entity/ViewPhraseUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/ui/ViewPhraseContent.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 911eededb2..0f1f035a13 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 @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.WalletAddressServiceRepository @@ -9,8 +10,8 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate +import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* @@ -372,4 +373,32 @@ internal object WalletsDomainModule { walletsRepository = walletsRepository, ) } + + @Provides + @Singleton + fun providesUnlockHotWalletContextualUseCase( + hotWalletAccessor: HotWalletAccessor, + ): UnlockHotWalletContextualUseCase { + return UnlockHotWalletContextualUseCase( + hotWalletAccessor = hotWalletAccessor, + ) + } + + @Provides + @Singleton + fun providesClearHotWalletContextualUnlockUseCase( + hotWalletAccessor: HotWalletAccessor, + ): ClearHotWalletContextualUnlockUseCase { + return ClearHotWalletContextualUnlockUseCase( + hotWalletAccessor = hotWalletAccessor, + ) + } + + @Provides + @Singleton + fun providesExportSeedPhraseUseCase(hotWalletAccessor: HotWalletAccessor): ExportSeedPhraseUseCase { + return ExportSeedPhraseUseCase( + hotWalletAccessor = hotWalletAccessor, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt index af00ff8af8..237bc5ebd3 100644 --- a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -32,6 +32,13 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk { override suspend fun exportBackup(unlockHotWallet: UnlockHotWallet): ByteArray = callSdk { exportBackup(unlockHotWallet) } + override suspend fun clearUnlockContext(hotWalletId: HotWalletId) { + callSdk { clearUnlockContext(hotWalletId) } + } + + override suspend fun getContextUnlock(unlockHotWallet: UnlockHotWallet): UnlockHotWallet = + callSdk { getContextUnlock(unlockHotWallet) } + override suspend fun delete(id: HotWalletId) = callSdk { delete(id) } override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = 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 5276185422..9525c29fa0 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 @@ -23,6 +23,7 @@ import com.tangem.features.hotwallet.CreateWalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.ViewPhraseComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -109,6 +110,7 @@ internal class ChildFactory @Inject constructor( private val walletActivationComponentFactory: WalletActivationComponent.Factory, private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, + private val viewPhraseComponentFactory: ViewPhraseComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, @@ -532,6 +534,15 @@ internal class ChildFactory @Inject constructor( componentFactory = updateAccessCodeComponentFactory, ) } + is AppRoute.ViewPhrase -> { + createComponentChild( + context = context, + params = ViewPhraseComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = viewPhraseComponentFactory, + ) + } is AppRoute.SendEntryPoint -> { 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 7e98053f5f..06cede4e5a 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 @@ -334,6 +334,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}") + @Serializable + data class ViewPhrase( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}") + @Serializable data class SendEntryPoint( val userWalletId: UserWalletId, diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index 796bc8d3ba..70273f2684 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -9,15 +9,26 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject +import kotlin.collections.set class DefaultHotWalletAccessor @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletPasswordRequester: HotWalletPasswordRequester, private val walletsRepository: WalletsRepository, + dispatchers: CoroutineDispatcherProvider, ) : HotWalletAccessor { + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + private var contextualUnlockHotWallet: ConcurrentHashMap = ConcurrentHashMap() + override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = hotSdkRequest(hotWalletId) { unlock -> tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign) @@ -30,6 +41,24 @@ class DefaultHotWalletAccessor @Inject constructor( tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request) } + override suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo { + val unlockHotWallet = contextualUnlockHotWallet[hotWalletId] ?: hotSdkRequest(hotWalletId) { it } + return tangemHotSdk.exportMnemonic(unlockHotWallet = unlockHotWallet) + } + + override suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet = hotSdkRequest(hotWalletId) { + tangemHotSdk.getContextUnlock(it).also { unlockHotWallet -> + contextualUnlockHotWallet[hotWalletId] = unlockHotWallet + } + } + + override fun clearContextualUnlock(hotWalletId: HotWalletId) { + contextualUnlockHotWallet[hotWalletId] = null + scope.launch { + tangemHotSdk.clearUnlockContext(hotWalletId) + } + } + private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { val isAccessCodeRequired = walletsRepository.requireAccessCode() 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 index 18d9d792fe..f0297ced18 100644 --- 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 @@ -7,4 +7,10 @@ interface HotWalletAccessor { suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse + + suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo + + suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet + + fun clearContextualUnlock(hotWalletId: HotWalletId) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearHotWalletContextualUnlockUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearHotWalletContextualUnlockUseCase.kt new file mode 100644 index 0000000000..be1a030837 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearHotWalletContextualUnlockUseCase.kt @@ -0,0 +1,17 @@ + +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.model.HotWalletId + +class ClearHotWalletContextualUnlockUseCase( + private val hotWalletAccessor: HotWalletAccessor, +) { + + operator fun invoke(hotWalletId: HotWalletId): Either { + return Either.catch { + hotWalletAccessor.clearContextualUnlock(hotWalletId) + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ExportSeedPhraseUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ExportSeedPhraseUseCase.kt new file mode 100644 index 0000000000..465a2560c3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ExportSeedPhraseUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.hot.sdk.model.SeedPhrasePrivateInfo + +class ExportSeedPhraseUseCase( + private val hotWalletAccessor: HotWalletAccessor, +) { + + suspend operator fun invoke(hotWalletId: HotWalletId): Either { + return Either.catch { + hotWalletAccessor.exportSeedPhrase(hotWalletId) + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockHotWalletContextualUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockHotWalletContextualUseCase.kt new file mode 100644 index 0000000000..982821ce5b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockHotWalletContextualUseCase.kt @@ -0,0 +1,18 @@ + +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.hot.sdk.model.UnlockHotWallet + +class UnlockHotWalletContextualUseCase( + private val hotWalletAccessor: HotWalletAccessor, +) { + + suspend operator fun invoke(hotWalletId: HotWalletId): Either { + return Either.catch { + hotWalletAccessor.unlockContextual(hotWalletId) + } + } +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 4106c3d8c6..abae1d96ad 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -149,7 +149,7 @@ private fun WalletBlock( .padding(top = 8.dp) .clip(TangemTheme.shapes.roundedCornersXMedium) .background( - color = TangemTheme.colors.background.secondary, + color = TangemTheme.colors.field.primary, shape = TangemTheme.shapes.roundedCornersXMedium, ) .clickable(onClick = onClick) diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/ViewPhraseComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/ViewPhraseComponent.kt new file mode 100644 index 0000000000..99e7dcf23f --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/ViewPhraseComponent.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 ViewPhraseComponent : 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/viewphrase/DefaultViewPhraseComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/DefaultViewPhraseComponent.kt new file mode 100644 index 0000000000..d82fbd968e --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/DefaultViewPhraseComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.hotwallet.viewphrase + +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.ViewPhraseComponent +import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel +import com.tangem.features.hotwallet.viewphrase.ui.ViewPhraseContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultViewPhraseComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: ViewPhraseComponent.Params, +) : ViewPhraseComponent, AppComponentContext by context { + private val model: ViewPhraseModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ViewPhraseContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : ViewPhraseComponent.Factory { + override fun create( + context: AppComponentContext, + params: ViewPhraseComponent.Params, + ): DefaultViewPhraseComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/di/ViewPhraseModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/di/ViewPhraseModule.kt new file mode 100644 index 0000000000..e6b6d7d4ab --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/di/ViewPhraseModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.viewphrase.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.ViewPhraseComponent +import com.tangem.features.hotwallet.viewphrase.DefaultViewPhraseComponent +import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel +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 ViewPhraseModule { + + @Binds + @IntoMap + @ClassKey(ViewPhraseModel::class) + fun bindViewPhraseModel(model: ViewPhraseModel): Model + + @Binds + fun bindViewPhraseComponentFactory(factory: DefaultViewPhraseComponent.Factory): ViewPhraseComponent.Factory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/entity/ViewPhraseUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/entity/ViewPhraseUM.kt new file mode 100644 index 0000000000..072bed311d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/entity/ViewPhraseUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet.viewphrase.entity + +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal data class ViewPhraseUM( + val onBackClick: () -> Unit, + val words: ImmutableList = persistentListOf(), +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt new file mode 100644 index 0000000000..f2d3adf53e --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt @@ -0,0 +1,76 @@ +package com.tangem.features.hotwallet.viewphrase.model + +import androidx.compose.runtime.Stable +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.decompose.navigation.Router +import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase +import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.hotwallet.ViewPhraseComponent +import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class ViewPhraseModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase, + private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase, +) : Model() { + + private val params = paramsContainer.require() + + private var hotWalletId: HotWalletId? = null + + internal val uiState: StateFlow + field = MutableStateFlow( + ViewPhraseUM( + onBackClick = { router.pop() }, + ), + ) + + init { + loadSeedPhrase() + } + + private fun loadSeedPhrase() { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + hotWalletId = userWallet.hotWalletId + modelScope.launch { + val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) + .getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") } + .mnemonic + .mnemonicComponents + uiState.update { + it.copy( + words = words.mapIndexed { index, s -> + EnumeratedTwoColumnGridItem(index + 1, s) + }.toImmutableList(), + ) + } + } + } + } + + override fun onDestroy() { + hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) } + super.onDestroy() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/ui/ViewPhraseContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/ui/ViewPhraseContent.kt new file mode 100644 index 0000000000..8768e09a55 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/ui/ViewPhraseContent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.hotwallet.viewphrase.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.components.appbar.AppBarWithBackButton +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.viewphrase.entity.ViewPhraseUM +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ViewPhraseContent(state: ViewPhraseUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.common_backup), + onBackClick = state.onBackClick, + ) + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .imePadding() + .weight(1f), + ) { + TitleBlock( + state = state, + modifier = Modifier.padding(top = 20.dp), + ) + + EnumeratedTwoColumnGrid( + items = state.words, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 32.dp), + ) + } + } +} + +@Composable +private fun TitleBlock(state: ViewPhraseUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(horizontal = TangemTheme.dimens.size36) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResourceSafe(R.string.backup_seed_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResourceSafe( + R.string.backup_seed_caution, + state.words.size, + ), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + ViewPhraseContent( + state = ViewPhraseUM( + onBackClick = {}, + words = List(12) { + EnumeratedTwoColumnGridItem( + index = it + 1, + mnemonic = "word${it + 1}", + ) + }.toImmutableList(), + ), + ) + } +} \ No newline at end of file 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 20c094ed28..b7357044d5 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 @@ -1,38 +1,33 @@ package com.tangem.features.hotwallet.walletbackup.model -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.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.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.common.routing.AppRoute +import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase 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.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class WalletBackupModel @Inject constructor( paramsContainer: ParamsContainer, - getWalletUseCase: GetUserWalletUseCase, - private val router: Router, + private val getWalletUseCase: GetUserWalletUseCase, + private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, override val dispatchers: CoroutineDispatcherProvider, - @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val router: Router, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -57,34 +52,16 @@ internal class WalletBackupModel @Inject constructor( ), ) - private val makeBackupAtFirstAlertBS - get() = bottomSheetMessage { - infoBlock { - icon(R.drawable.ic_passcode_lock_32) { - type = MessageBottomSheetUMV2.Icon.Type.Accent - backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint - } - title = resourceReference(R.string.hw_backup_need_title) - body = resourceReference(R.string.hw_backup_need_description) - } - secondaryButton { - text = resourceReference(R.string.hw_backup_need_action) - onClick { - router.push(AppRoute.CreateWalletBackup(params.userWalletId)) - closeBs() - } - } - } - init { - getWalletUseCase.invokeFlow(params.userWalletId) - .map { it.getOrNull() } - .distinctUntilChanged() - .filterNotNull() - .onEach { - updateBackupStatuses(it) - } - .launchIn(modelScope) + getWalletUseCase.invoke(params.userWalletId) + .fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { + updateBackupStatuses(it) + }, + ) } private fun updateBackupStatuses(userWallet: UserWallet) { @@ -118,9 +95,37 @@ internal class WalletBackupModel @Inject constructor( private fun onRecoveryPhraseClick() { if (uiState.value.backedUp) { - // TODO [REDACTED_TASK_KEY] + getWalletUseCase.invoke(params.userWalletId) + .fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + when (userWallet) { + is UserWallet.Cold -> { + val userWalletId = userWallet.walletId + Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId") + } + is UserWallet.Hot -> showSeedPhrase(userWallet) + } + }, + ) } else { - uiMessageSender.send(makeBackupAtFirstAlertBS) + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + } + } + + private fun showSeedPhrase(hotWallet: UserWallet.Hot) { + modelScope.launch { + unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId) + .fold( + ifLeft = { + Timber.e("Error while export seed phrase: $it") + }, + ifRight = { seedPhrasePrivateInfo -> + router.push(AppRoute.ViewPhrase(params.userWalletId)) + }, + ) } } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 72c784bae5..80bdb6d116 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ 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 ^ -tangemHotSdk = "develop-525" +tangemHotSdk = "develop-526" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 94a44864437c8400b83d969cd5f4dd02176d3140 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Sep 2025 13:19:12 +0300 Subject: [PATCH 54/69] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 10 +- .../di/UserWalletsListManagerModule.kt | 3 + .../DefaultUserWalletsListRepository.kt | 108 +++++++++------- .../utils/UserWalletEncyptionKeyCalculator.kt | 4 + .../converter/UserWalletItemUMConverter.kt | 8 +- .../local/preferences/PreferencesKeys.kt | 6 +- core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-es/strings.xml | 119 +++++++++--------- core/res/src/main/res/values-fr/strings.xml | 18 ++- core/res/src/main/res/values-ru/strings.xml | 13 +- .../src/main/res/values-uk-rUA/strings.xml | 12 +- core/res/src/main/res/values/strings.xml | 93 +++++++++++++- .../settings/DefaultSettingsRepository.kt | 8 +- ...eCase.kt => SetAskBiometryShownUseCase.kt} | 4 +- ...ase.kt => ShouldShowAskBiometryUseCase.kt} | 4 +- .../repositories/SettingsRepository.kt | 4 +- .../biometry/impl/model/AskBiometryModel.kt | 6 +- .../details/model/UserWalletListModel.kt | 2 +- .../hotwallet/accesscode/AccessCodeModel.kt | 61 +++++++++ .../entry/impl/model/OnboardingEntryModel.kt | 2 +- .../model/MultiWalletFinalizeModel.kt | 14 ++- .../wallet/utils/UserWalletsFetcher.kt | 2 +- .../wallet/child/wallet/model/WalletModel.kt | 19 ++- .../model/WalletsUpdateActionResolver.kt | 14 +-- .../MultiWalletWarningsSubscriber.kt | 6 + .../wallet/utils/DefaultUserWalletsFetcher.kt | 23 +++- .../connections/utils/WcUserWalletsFetcher.kt | 2 +- .../welcome/impl/model/WelcomeModel.kt | 2 +- 28 files changed, 393 insertions(+), 178 deletions(-) rename domain/settings/src/main/java/com/tangem/domain/settings/{SetSaveWalletScreenShownUseCase.kt => SetAskBiometryShownUseCase.kt} (71%) rename domain/settings/src/main/java/com/tangem/domain/settings/{ShouldShowSaveWalletScreenUseCase.kt => ShouldShowAskBiometryUseCase.kt} (57%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index df25dd1255..2cc898457b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -54,8 +54,8 @@ internal object SettingsDomainModule { @Singleton fun providesShouldShowSaveWalletScreenUseCase( settingsRepository: SettingsRepository, - ): ShouldShowSaveWalletScreenUseCase { - return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository) + ): ShouldShowAskBiometryUseCase { + return ShouldShowAskBiometryUseCase(settingsRepository = settingsRepository) } @Provides @@ -138,10 +138,8 @@ internal object SettingsDomainModule { @Provides @Singleton - fun provideSetSaveWalletScreenShownUseCase( - settingsRepository: SettingsRepository, - ): SetSaveWalletScreenShownUseCase { - return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository) + fun provideSetSaveWalletScreenShownUseCase(settingsRepository: SettingsRepository): SetAskBiometryShownUseCase { + return SetAskBiometryShownUseCase(settingsRepository = settingsRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index c521709443..f582dea772 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -122,6 +123,7 @@ internal object UserWalletsListManagerModule { passwordRequester: HotWalletPasswordRequester, appPreferencesStore: AppPreferencesStore, hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + scanCardProcessor: ScanCardProcessor, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -168,6 +170,7 @@ internal object UserWalletsListManagerModule { appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, + scanCardProcessor = scanCardProcessor, ) } 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 87edec2202..c6204135b7 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 @@ -11,6 +11,7 @@ import com.tangem.common.map import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -38,6 +39,9 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import com.tangem.core.analytics.models.AnalyticsParam +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock @Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( @@ -50,36 +54,45 @@ internal class DefaultUserWalletsListRepository( private val savePersistentInformation: ProviderSuspend, private val appPreferencesStore: AppPreferencesStore, private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + private val scanCardProcessor: ScanCardProcessor, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) override val selectedUserWallet = MutableStateFlow(null) + private val mutex = Mutex() override suspend fun load() { - if (userWallets.value != null) return + mutex.withLock { + if (userWallets.value != null) return - if (savePersistentInformation().not()) { - // If we don't save persistent information, we don't need to load user wallets - // and we should clear any existing data - clearPersistentData() - updateWallets { emptyList() } - return - } - - val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured() - - publicInformationRepository.getAll() - .map { it.toUserWallets() } - .flatMap { wallets -> - sensitiveInformationRepository.getAll(unsecuredEncryptionKeys) - .map { wallets.updateWith(it) } - }.doOnSuccess { - userWallets.value = it + if (savePersistentInformation().not()) { + // If we don't save persistent information, we don't need to load user wallets + // and we should clear any existing data + clearPersistentData() + updateWallets { emptyList() } + return } - val selectedUserWalletId = selectedUserWalletRepository.get() - selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId } - ?: userWallets.value?.firstOrNull() + val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured() + + publicInformationRepository.getAll() + .map { it.toUserWallets() } + .flatMap { wallets -> + sensitiveInformationRepository.getAll(unsecuredEncryptionKeys) + .map { wallets.updateWith(it) } + } + .doOnSuccess { loadedWallets -> + userWallets.update { toUpdate -> + val selectedUserWalletId = selectedUserWalletRepository.get() + selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId } + ?: loadedWallets.firstOrNull()?.also { + selectedUserWalletRepository.set(it.walletId) + } + + loadedWallets + } + } + } } override suspend fun userWalletsSync(): List { @@ -191,18 +204,17 @@ internal class DefaultUserWalletsListRepository( userWalletEncryptionKeysRepository.delete(userWalletIds) - val userWalletsBeforeDelete = userWallets.value ?: return@either - userWallets.update { currentWallets -> - currentWallets?.filterNot { it.walletId in userWalletIds } - } - - selectedUserWallet.update { currentSelected -> - if (currentSelected == null) return@update null - - userWallets.value?.findAvailableUserWallet( - userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0, - ) + val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() } + selectedUserWallet.update { currentSelected -> + if (currentSelected == null) return@update null + val newSelected = updatedWallets?.findAvailableUserWallet( + currentWallets.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0, + ) + selectedUserWalletRepository.set(newSelected?.walletId) + newSelected + } + updatedWallets } } @@ -258,7 +270,7 @@ internal class DefaultUserWalletsListRepository( raise(UnlockWalletError.UnableToUnlock) } - tangemSdkManagerProvider().scanProduct() + scanCardProcessor.scan(analyticsSource = AnalyticsParam.ScreensSources.SignIn) .doOnSuccess { scanResponse -> val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build() @@ -266,9 +278,14 @@ internal class DefaultUserWalletsListRepository( raise(UnlockWalletError.ScannedCardWalletNotMatched) } - saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true) - .mapLeft { UnlockWalletError.UnableToUnlock } - .bind() + val encryptionKey = UserWalletEncryptionKey( + walletId = userWallet.walletId, + encryptionKey = scanResponse.encryptionKey ?: raise(UnlockWalletError.UnableToUnlock), + ) + + sensitiveInformationRepository.getAll(listOf(encryptionKey)) + .doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } + .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) } } .doOnFailure { raise(UnlockWalletError.UserCancelled) @@ -278,7 +295,8 @@ internal class DefaultUserWalletsListRepository( } override suspend fun unlockAllWallets(): Either = either { - val userWalletIds = userWalletsSync().map { it.walletId }.toSet() + val userWallets = userWalletsSync() + val userWalletIds = userWallets.map { it.walletId }.toSet() val biometricKeys = runCatching { userWalletEncryptionKeysRepository.getAllBiometric() }.getOrElse { @@ -291,7 +309,7 @@ internal class DefaultUserWalletsListRepository( val unlockedWalletsIds = allKeys.map { it.walletId } val unlockedWallets = unlockedWalletsIds.mapNotNull { id -> - userWalletsSync().firstOrNull { it.walletId == id } + userWallets.firstOrNull { it.walletId == id } } // Remove all password attempts for unlocked hot wallets @@ -306,7 +324,7 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(allKeys) .doOnSuccess { sensitiveInfo -> - updateWallets { it?.updateWith(sensitiveInfo) } + updateWallets { userWallets.updateWith(sensitiveInfo) } } .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } @@ -390,11 +408,13 @@ internal class DefaultUserWalletsListRepository( } 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 } + userWallets.update { + val updated = block(it) + selectedUserWallet.update { currentSelected -> + if (currentSelected == null) return@update null + updated?.find { it.walletId == currentSelected.walletId } + } + updated } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt index 8b05c741e7..65b8e925a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt @@ -4,6 +4,7 @@ import com.tangem.common.extensions.calculateSha256 import com.tangem.domain.common.extensions.calculateHmacSha256 import com.tangem.domain.models.MobileWallet import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet internal val UserWallet.encryptionKey: ByteArray? @@ -19,6 +20,9 @@ private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray { return message.calculateHmacSha256(keyHash) } +val ScanResponse.encryptionKey: ByteArray? + get() = findPublicKey(this.card.wallets)?.let { calculateEncryptionKey(it) } + private fun findPublicKey(wallets: List): ByteArray? { return wallets.firstOrNull()?.publicKey } 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 aec881c569..a079675da1 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 @@ -34,7 +34,7 @@ class UserWalletItemUMConverter( private val appCurrency: AppCurrency? = null, private val balance: TotalFiatBalance? = null, private val isBalanceHidden: Boolean = false, - private val authMode: Boolean = false, + private val isAuthMode: Boolean = false, private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, artwork: UserWalletItemUM.ImageState? = null, ) : Converter { @@ -58,11 +58,11 @@ class UserWalletItemUMConverter( } private fun isEnabled(userWallet: UserWallet): Boolean { - return authMode || userWallet.isLocked.not() + return isAuthMode || userWallet.isLocked.not() } private fun getLabelOrNull(userWallet: UserWallet): LabelUM? { - return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) { + return if (isAuthMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) { LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, @@ -92,7 +92,7 @@ class UserWalletItemUMConverter( private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance { return when { userWallet.isLocked -> UserWalletItemUM.Balance.Locked - authMode -> UserWalletItemUM.Balance.NotShowing + isAuthMode -> UserWalletItemUM.Balance.NotShowing isBalanceHidden -> UserWalletItemUM.Balance.Hidden balance == null -> UserWalletItemUM.Balance.Loading else -> { 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 8644fb72c8..63fbdfebf8 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 @@ -7,7 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACC import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY @@ -26,7 +26,7 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } - val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } + val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } @@ -183,7 +183,7 @@ object PreferencesKeys { internal fun getTapPrefKeysToMigrate(): Set { return setOf( SAVE_USER_WALLETS_KEY, - SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, + SHOULD_SHOW_ASK_BIOMETRY_KEY, APP_LAUNCH_COUNT_KEY, SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY, FUNDS_FOUND_DATE_KEY, diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index d0bb34dd95..b7cc837f73 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -452,7 +452,7 @@ Aktuelle Preise werden abgerufen ... Variabler Zinssatz Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s - Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden + Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden. Weitere Anbieter folgen in Kürze Anbieter Bester Preis @@ -1039,7 +1039,7 @@ Senden... Berühre an beliebiger Stelle für Änderungen Versende %s - Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s + Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s. Du sendest **%1$s** und %2$s Du sendest **%1$s** Die Netzwerkgebühr wird durch die Nutzung von %1$s Energieträgern gedeckt. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1e084899ad..16cac3a4f8 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -26,7 +26,7 @@ Gestionar tokens Envíe solo %1$s (%2$s) desde redes como %3$s a esta dirección. Usar otros tokens y redes puede resultar en la pérdida de fondos. Por defecto - Legado + Legacy Cómo escanear Solicitar soporte Inténtelo de nuevo @@ -65,8 +65,8 @@ Guarde estas %s palabras en un lugar seguro, como un administrador de contraseñas, y nunca las comparta con nadie. No se puede restaurar Frase de recuperación - Escriba estas palabras %s en orden y guárdelas en un lugar privado y seguro - La responsabilidad total de la seguridad y la copia de seguridad de la billetera y la frase de recuperación recae en el usuario, no en Tangem. + Escriba estas %s palabras en orden y guárdelas en un lugar privado y seguro + La responsabilidad total sobre la seguridad y copia de seguridad de la billetera y su frase de recuperación recae en el usuario, no en Tangem. Frase de recuperación Para ocultar o mostrar tus saldos, simplemente gira la pantalla de tu dispositivo hacia abajo, o desactívalo en Ajustes No mostrar de nuevo @@ -118,7 +118,7 @@ Requisitos de transacción de Cardano Para realizar una %1$s transacción, deba depositar algo de ADA para cubrir la tarifa de red y el valor mínimo de ADA (se recomiendan 5 ADA) ADA insuficiente para la transferencia de tokens - El monto enviado y el cambio no pueden ser menores a 1 ADA + La cantidad enviada y el cambio no pueden ser menores a 1 ADA Deba mantener algo de ADA porque tiene algunos tokens en la blockchain de Cardano ADA insuficiente Aceptar @@ -147,9 +147,9 @@ Cancelar Cambie Elige una acción - Elegir red - Elige token - Elegir wallet + Elija red + Elija token + Elija wallet Reclamar Reclame recompensas Cerrar @@ -221,7 +221,7 @@ Anillo primario Frase de contraseña Pegar - Política de privacidad + Política de privacidad %1$s-%2$s %1$s — %2$s Leer más @@ -254,10 +254,10 @@ Enviar Con éxito Soporte - Redes compatibles + Redes soportadas Swap términos y condiciones - Condiciones de uso + Condiciones de uso Hoy %d token @@ -271,7 +271,7 @@ Hubo un error. Por favor inténtelo de nuevo. Inaccesible Termine el staking - Debido a limitaciónes sobre %1$s, solos %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. + Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. Valor copiado semana con @@ -334,7 +334,8 @@ Recibir activos %s dirección Enviar activos a otras redes resultará en una pérdida permanente. - Envíe fondos utilizando solo + Red de %s + Envíe fondos utilizando solo la Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera @@ -377,7 +378,7 @@ Esperando depósito Esperando depósito Reembolso completado - A la espera del reembolso + Esperando reembolso Transfiriendo a su cuenta Enviando fondos... Fondos enviados @@ -389,12 +390,12 @@ Buscar tarifas actuales... Tasa flotante Al utilizar la función de intercambio, acepta el %s - Al utilizar la función de intercambio, aceptas las condiciones de %1$s y %2$s del proveedor + Al utilizar la función de intercambio, aceptas las condiciones de %1$s y %2$s del proveedor. Pronto habrá más proveedores disponibles Proveedor Mejor tarifa Lista de advertencias de la FCA - Proveedor en la lista de advertencia de la FCA + Proveedor en la lista de advertencias de la FCA Disponible hasta %s Disponible desde %s No disponible para este par @@ -407,7 +408,7 @@ Una vez oculto, el estado de la transacción no se puede volver a ver. En su lugar, basta con deslizar el dedo para descartarla. ¿Ocultar el estado de la transacción? Este token no es compatible. Por favor, elige un token diferente para intercambiar. - %s no es compatible + %s no está soportado Intercambiar con No se encontraron fichas. Por favor intenta con otra solicitud ID : %s @@ -436,8 +437,8 @@ Para continuar, de autorización a los smart contracts de %1s para usar su %2s Dar autorización Ilimitado - Agregar billetera existente - Crear nueva billetera + Agregar Billetera Existente + Crear Nueva Billetera Pedir Tangem Escanee a %s @@ -524,7 +525,7 @@ Top Ganadores Top Perdedores Tendencias - Staking es la forma más fácil de recibir recompensas por tus criptomonedas. %s + Staking es la forma más fácil de recibir recompensas por sus criptomonedas. %s Gana hasta %s APY Acerca de %s @@ -605,14 +606,14 @@ Colecciones de NFT Es posible que algunos datos no se carguen Problemas temporales de carga - Información de base + Información básica Cadena Dirección del contrato Cadena es la blockchain en la que está el NFT. La dirección del contrato es un identificador único para el contrato inteligente que gobierna los tokens en la blockchain Una etiqueta que describe lo raro que es el NFT. Cuanto menor sea el valor, más único será el NFT. La posición de un NFT en el ranking de rareza entre otros tokens. Cuanto más alto sea el ranking, más raro y valioso será el NFT. - La dirección del contrato es un identificador único para el contrato inteligente que gobierna los tokens en la blockchain + La dirección del token es un identificador único del token en la blockchain, que permite el seguimiento de las transacciones y la propiedad El ID del token es un identificador único asignado a cada token, que lo distingue de otros en la colección. El estándar de token define qué tipo de token es y cómo funciona con diferentes billeteras y plataformas Último precio de venta @@ -642,13 +643,13 @@ Pulse aquí para recibir el primer NFT Colecciones de NFT No se pueden cargar los datos - Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloquea y oculta ese monto indefinidamente + Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloqueará y ocultará esa cantidad indefinidamente La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. Para crear una cuenta, envíe fondos a esta dirección - La cuenta de destino no tiene una Trustline para el activo que se envía. - Únete ahora - Comparte tu código y gana 5 USDT por venta. Tu amigo obtiene un 10% de descuento. - ¡Obtén RECOMPENSAS por cada amigo! + La cuenta de destino no tiene una Trustline (línea de confianza) para el activo que se envía. + Únase ahora + Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento. + ¡Obtenga RECOMPENSAS por cada amigo! Deba configurar un único código de acceso para proteger todss sus dispositivos. Proteger Puede configurar un código de acceso individual en cada tarjeta más adelante @@ -775,11 +776,11 @@ Organizar tokens Desagrupar Más información - Puedes activar las Notificaciones para Tangem en Ajustes. + Puede activar las Notificaciones para Tangem en Ajustes. Activar más tarde Ajustes Activar notificaciones - Reciba alertas de transacciones entrantes en las redes compatibles + Reciba alertas de transacciones entrantes en las redes soportadas Notificaciones de transacciones Seleccione de la galería Ajustes @@ -869,11 +870,11 @@ Introduce la dirección Nombre ENS o dirección La dirección es la misma que su billetera. - El monto mínimo es %s + La cantidad mínima es %s El cambio mínimo es %s Tarifa no válida El saldo mínimo es %s - No se ha creado la cuenta de destino. El monto a enviar debe ser %s + comisiones o más + No se ha creado la cuenta de destino. La cantidad a enviar debe ser %s + comisiones o más Error desconocido Memo no válido. No se añadirá a la transacción. Memo @@ -893,7 +894,7 @@ Memo no válido Cobertura de tarifa de red Nonce - Número único para cada transacción. Utilízalo para reenviar o cancelar una transacción pendiente. + Número único para cada transacción. Utilícelo para reenviar o cancelar una transacción pendiente. Introduzca nonce.. Fondos insuficientes para la transferencia, ya que el total de la tarifa y el importe de la transferencia supera el saldo existente El total supera el saldo @@ -934,7 +935,7 @@ Enviando... Toque cualquier campo para editarlo Enviar %s - Está enviando **%1$s** incluida una tarifa de red de %2$s + Está enviando **%1$s** incluida una tarifa de red de %2$s. Está enviando**%1$s** y %2$s Está enviando **%1$s** La tarifa de red se cubrirá al usar %1$s energía @@ -943,18 +944,18 @@ A la dirección La transacción se firmó con éxito y se envió al nodo blockchain. El saldo de la billetera se actualizará después de un tiempo. %1$s es un activo en la red Tron. Para calcular la tarifa y realizar una transacción, deba depositar algo de Tron (TRX) en su cuenta. - El monto excede el saldo + La cantidad excede el saldo Es necesario una etiqueta de destino (memo) para completar esta transacción para la dirección especificada. Etiqueta de destino obligatoria Cantidad no válida La tarifa excede el saldo - El monto total excede el saldo + La cantidad total excede el saldo 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. + Elige cualquier token para recibir. Su destinatario recibirá exactamente lo que seleccionaste, sin problemas. El destinatario recibirá Al destinatario Cantidad a recibir @@ -966,7 +967,7 @@ Olvidar la billetera Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre - Haz que tu token trabaje por ti + Haga que su token trabaje para Ud. El monto del staking debe ser al menos %s El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red. @@ -1101,7 +1102,7 @@ Compatible con Web 3.0 Se requiere una transacción entrante de al menos %1$s para proceder Fondos insuficientes - Tasa fija + Tasa Fija La red cobrará una tarifa de aprobación de token para verificar que está autorizando el uso de su token para el swap. Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! @@ -1176,12 +1177,12 @@ desde: %s a: %s validador: %s - Las notificaciones están activadas, pero no funcionarán hasta que permitas las notificaciones en la configuración de tu dispositivo. + Las notificaciones están activadas, pero no funcionarán hasta que permita las notificaciones en la configuración de su dispositivo. Notificaciones de transacciones Mínimo %s El monto mínimo para realizar esta transacción es %1$s. - Las tarifas de la red Tron para tokens populares pueden ser más altas. Hacer staking de TRX puede ayudar a reducir los costos de transacción. - Ahorra en las tarifas de la red Tron + Las tarifas de la red Tron para tokens populares pueden ser más altas. Hacer staking de TRX puede ayudar a reducir costes de transacción. + Ahorre en las tarifas de la red Tron Inténtelo de nuevo Ha escaneado la misma tarjeta. Para crear una billetera gemela, necesite escanear la tarjeta con no. %d Ha escaneado la tarjeta gemela incorrecta. Por favor, intente con otra @@ -1196,7 +1197,7 @@ Toque la tarjeta gemela con el número %s y no la retira hasta el final de la operación Por favor, inténtelo de nuevo más tarde. Si el problema persiste, póngase en contacto con el servicio de asistencia. ¡Algo salió mal! - Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de asistencia. + Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte. Use %s o escanee una tarjeta/anillo para tener acceso a su billetera Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. Manténgase actualizado con las últimas funciones y noticias @@ -1260,7 +1261,7 @@ Accede a más de 13 000 criptomonedas. Compra, vende, intercambia y realiza staking con un solo toque.\nVincula hasta tres tarjetas para hacer copias de seguridad. Descubre Tangem Wallet Cambiar código de acceso - Manténteinformado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. + Manténgase informado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. Notificaciones de transacciones Establecer código de acceso Ajustes de la wallet @@ -1297,7 +1298,7 @@ El monto a recibir debe ser de al menos %s Esto puede suceder porque el proveedor actualmente no puede operar con el par que usted seleccionó. Espere un momento e inténtelo de nuevo. (Código %s) El par seleccionado no está disponible temporalmente - Algunos proveedores no están autorizados por la Autoridad de Conducta Financiera (FCA) del Reino Unido. Deberías evitar tratar con ellos. + Algunos proveedores no están autorizados por la Autoridad de Conducta Financiera del Reino Unido. Debe evitar tratar con ellos. Servicio no disponible temporalmente La cantidad de tokens a intercambiar no debe exceder %s El monto a cambiar debe ser de al menos %s @@ -1333,10 +1334,10 @@ Su opinión nos motiva a hacer Tangem Wallet aún mejor ¿Disfrutando de Tangem? Deba asociar su token antes de recibir tokens - Debes abrir una línea de confianza para tu token antes de recibirlo + Debe abrir una trustline para su token antes de recibirlo Se requiere tarifa de alquiler de red Acción requerida - ¿Te pusiste en contacto con el servicio de asistencia a través de la aplicación o por correo electrónico en los 7 días posteriores a la creación de la billetera? Si lo hiciste o no estás seguro, sigue y completa las instrucciones. + ¿Se puso en contacto con el servicio de soporte a través de la app en los 7 días posteriores a la creación de la billetera? Si lo hiciste o no estás seguro, pulse en \"Sí\" y complete las instrucciones. ¡Gracias! ¡Todo listo! No se requieren más acciones. Ahora será redirigido al sitio web oficial de Tangem. Por favor, lea y siga las instrucciones allí indicadas. ¿Alguna vez contactó al equipo de soporte de Tangem directamente desde esta aplicación? @@ -1353,16 +1354,16 @@ Esta es una tarjeta de Testnet. No puede procesar transacciones y solo deba usarse para pruebas y desarrollo. Solo para fines de prueba El saldo podría estar desactualizado. Actualice la página. - No hay suficiente %1$s. Recarga tu %2$s cuenta para asociar este token - Habilitar línea de confianza - Una línea de confianza debe estar habilitada para recibir este token. La red requiere un %1$s %2$s reserva. - Se requiere línea de confianza + No hay suficiente %1$s. Recargue su cuenta %2$s para asociar este token + Habilitar Trustline + Una trustline (línea de confianza) debe estar habilitada para recibir este token. La red requiere un %1$s %2$s reserva. + Se requiere trustline (línea de confianza) La red requerida %s no está añadida a su portafolio. Añádala primero y luego continúe con la conexión. Agregue red al portafolio Dominio malicioso Dominio desconocido Conectarse de todas formas - Error de tiempo de espera. Por favor, inténtalo de nuevo más tarde. + Error de tiempo de espera. Por favor, inténtelo de nuevo más tarde. Error al establecer WalletConnect Este dominio no puede ser verificado. Compruebe cuidadosamente la solicitud de aprobación. Para continuar, vuelva a conectar su sesión de dApp con la red requerida %s. @@ -1382,10 +1383,10 @@ Esta red %s no es compatible con Tangem Wallet y no puede conectarse. Red no compatible Actualmente, Tangem no es compatible con una red requerida por %s. - Redes no compatibles + Redes no soportadas Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. Dominio verificado - Se seleccionó una tarjeta o un anillo incorrectos en la app + Se seleccionó una tarjeta o un anillo incorrectos en la App Tenemos algún tipo de problema Todas las dApps desconectadas Permitir gastar @@ -1414,22 +1415,22 @@ Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva - Dominio de dApp no válido + Dominio de dApp incorrecto %s no especifica ninguna blockchain, ni obligatoria ni opcional. Asegúrese de haber utilizado el URI correcto Sin redes Por favor, genere una nueva URI e intente conectarse nuevamente Propuesta de conexión caducada Cambios estimados en la billetera La transacción no ha podido ser simulada. Por favor, proceda con precaución. - La estimación no es compatible con %s + La estimación no está soportada para %s Sugerido por %s Recargue su saldo para cubrir la tarifa de red Insuficiente %1$s Transacción maliciosa - Añada la red %s a su perfil para esta billetera + Añada la red %s a su portafolio para esta billetera La billetera no tiene las redes requeridas Nueva conexión - Conecta tu billetera a diferentes dApps + Conecte su billetera a diferentes dApps Sin sesiones No se han detectado cambios en la billetera Se han detectado riesgos potenciales o comportamiento malicioso. Conectarse o firmar transacciones puede resultar en la pérdida de fondos. @@ -1437,7 +1438,7 @@ Abra la aplicación Web3 y elija la opción WalletConnect Solicitud de Firmar de todos modos - Tipo de firma + Tipo de Firma Se requiere al menos una red para la conexión dApp Especificar las redes seleccionadas Firmado correctamente @@ -1461,6 +1462,6 @@ Use %s o escanee una tarjeta/anillo para acceder a la app ¡Bienvenido de nuevo! No, enviar todo - Réduire de %s XTZ - Para evitar pagar una comisión mayor la próxima vez que recargue su billetera, reduzca el monto en %s + Reducir en %s XTZ + Para evitar pagar una comisión mayor la próxima vez que recargue su billetera, reduzca el importe en %s XTZ diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 228fa133e0..0d2ee7b831 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -198,7 +198,7 @@ Bague principale Passphrase Coller - Politique de confidentialité + Politique de confidentialité %1$s-%2$s %1$s — %2$s En savoir plus @@ -234,7 +234,7 @@ Réseaux pris en charge Échanger termes et conditions - Conditions d\'utilisation + Conditions d\'utilisation Aujourd\'hui %d jeton @@ -311,6 +311,7 @@ Recevoir des actifs %s adresse L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive. + %s réseau Envoyez des fonds en utilisant uniquement Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s Erreur WalletConnect @@ -365,7 +366,7 @@ En cherche des meilleurs taux Taux flottant En utilisant la fonctionnalité d\'échange, vous acceptez les %s - En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur + En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur. D\'autres fournisseurs arriveront bientôt.\nRestez branchés ! Fournisseur Meilleur taux @@ -908,7 +909,7 @@ En cours d\'envoi Appuyez sur n\'importe quel champ pour le modifier Envoyer %s - Vous envoyez **%1$s** incluant des frais de réseau de %2$s + Vous envoyez **%1$s** incluant des frais de réseau de %2$s. Vous envoyez **%1$s** et %2$s Vous envoyez **%1$s** les frais de réseau seront couverts en utilisant %1$s énergie @@ -923,11 +924,20 @@ Montant invalide Les frais de commissions dépassent le solde Le total dépasse le solde + Êtes-vous sûr de vouloir modifier le token de réception ? Cela réinitialisera les données que vous avez saisies précédemment. + Changement de token Échanger et envoyer + Poursuivre l\'échange ? Cela effacera vos données précédentes. + Confirmer la conversion + L\'envoi de toute autre crypto entraînera sa perte irréversible. + Sélectionnez le réseau destinataire approprié. Envoyez n\'importe quel jeton et nous le convertirons en cours de route. Votre destinataire reçoit exactement ce dont il a besoin, en toute simplicité. Le destinataire recevra Un destinataire sera envoyé Montant à recevoir + Le destinataire reçoit %s + Êtes-vous sûr de vouloir annuler la conversion ? Vos données précédentes seront effacées. + Supprimer la conversion Envoyer avec swap Transaction envoyée Scannez la carte/ bague que vous souhaitez configurer. diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 35ce29d63a..34e2073bb1 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,6 +4,7 @@ Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Аккаунт Новый аккаунт + Основной аккаунт Продолжить редактирование Несохраненные изменения Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки. @@ -204,7 +205,7 @@ Основное кольцо Парольная фраза Вставить - Политикой конфиденциальности + Политикой конфиденциальности %1$s-%2$s %1$s — %2$s Подробнее @@ -237,7 +238,7 @@ Поддержка Обменять условия участия - Условиями использования + Условиями использования Сегодня %d токен @@ -316,7 +317,7 @@ Получить активы %s адрес Отправка средств в другой сети может повлечь потерю средств. - %s сети + %s сеть Отправляйте средства, используя только Привет, команда поддержки, у меня возникла ошибка с кодом: %s Ошибка WalletConnect @@ -337,7 +338,7 @@ Обмен через %s Чтобы вернуть ваши деньги, посетите сайт провайдера Операция не выполнена провайдером - Ваш обмен занимает больше времени, чем ожидалось. Пожалуйста, обратитесь в службу поддержки провайдера для уточнения. + Ваш обмен занимает больше времени, чем обычно, но ваши средства полностью в безопасности и будут доставлены. Если возникли вопросы, вы всегда можете связаться с поддержкой провайдера. Отправленные средства были возвращены в %1$s на ваш кошелек в соответствии с правилами OKX или моста обмена. %2$s Сумма была возвращена в %1$s (%2$s сети) Посетите сайт провайдера для проверки @@ -370,7 +371,7 @@ Получение наилучших курсов... Плавающая ставка Пользуясь сервисом, вы соглашаетесь с %s - Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s + Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s. Больше провайдеров на подходе.\nСледите за обновлениями! Провайдер Лучший курс @@ -929,7 +930,7 @@ Отправка Нажмите на любое поле, чтобы изменить его Отправка %s - Вы отправляете **%1$s**, включая комиссию сети %2$s + Вы отправляете **%1$s**, включая комиссию сети %2$s. Вы отправляете **%1$s** и %2$s Вы отправляете **%1$s** Комиссия сети будет покрыта за счет использования %1$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 709edd7c9e..33db0a745b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -173,6 +173,7 @@ Синхронізувати адреси Перейти до провайдера Перейти до токену + Зrozуміло Приховати година Імпортувати @@ -196,7 +197,7 @@ Основне кільце Парольна фраза Вставити - Політикою конфіденційності + Політикою конфіденційності %1$s-%2$s %1$s — %2$s Детальніше @@ -230,7 +231,7 @@ Підтримка Обмін умови участі - Умовами використання + Умовами використання Сьогодні %d токен @@ -307,6 +308,7 @@ Отримати активи %s адреса Надсилання активів в інші мережі призведе до безповоротної втрати. + %s мережа Надсилайте кошти, використовуючи лише Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s Помилка WalletConnect @@ -327,7 +329,7 @@ Обмін через %s Щоб повернути ваші кошти, відвідайте сайт провайдера Операція не виконана провайдером - Ваш обмін триває довше, ніж очікувалося. Будь ласка, зверніться до служби підтримки постачальника послуг, для уточнення. + Ваш обмін триває довше, ніж зазвичай, але ваші кошти повністю в безпеці та будуть доставлені. Якщо ви хвилюєтесь, ви завжди можете звернутися до служби підтримки провайдера. Тривалий час транзакції Сума транзакції була повернута в %1$s на ваш гаманець відповідно до правил OKX або мосту обміну. %2$s Сума була повернута в %1$s (%2$s мережі) @@ -361,7 +363,7 @@ Шукаємо найвигідніший курс... Плаваюча ставка Використовуючи сервіс обміну, ви погоджуєтеся з його %s - Використовуючи сервіс обміну, ви погоджуєтеся з його %1$s та %2$s + Використовуючи сервіс обміну, ви погоджуєтеся з його %1$s та %2$s. Незабаром з\'являться нові провайдери.\nСлідкуйте за новинами! Провайдер Найкращий курс @@ -910,7 +912,7 @@ Надсилання... Торкніться будь-якого поля, щоб змінити його Надіслати %s - Ви надсилаєте **%1$s**, включно з комісію мережі %2$s + Ви надсилаєте **%1$s**, включно з комісію мережі %2$s. Ви надсилаєте **%1$s** і %2$s Ви надсилаєте **%1$s** комісія мережі буде покрита, використовуючи %1$s енергію diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 551ec44606..7d747732ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ Add account Edit account %1$s in %2$s + Main account Account recovered Long tap on an account to reorder accounts Keep Editing @@ -182,6 +183,7 @@ Allow Amount Analytics + and Apply Approval Approve @@ -255,6 +257,7 @@ Import In progress Later + Learn more %1$s left Legacy Bitcoin Locked @@ -280,7 +283,7 @@ Primary ring Passphrase Paste - Privacy Policy + Privacy Policy %1$s-%2$s %1$s — %2$s Read more @@ -319,7 +322,7 @@ Tangem Tangem Wallet terms and conditions - Terms of Use + Terms of Use Today %d token @@ -419,7 +422,7 @@ Exchange by %s Visit provider’s website to refund your money Operation failed by provider - Your swap is taking longer than expected. Please go to provider support for assistance. + Your swap is taking longer than usual, but your funds are completely safe and will be delivered. For any questions, you can contact the provider’s support team. Long transaction time The transaction amount was refunded in %1$s to your wallet due to OKX or bridge rules. %2$s The amount was refunded in %1$s (%2$s network) @@ -453,7 +456,7 @@ Fetching current rates... Floating rate By using swap functionality, you agree with provider’s %s - By using swap functionality, you agree with provider’s %1$s and %2$s + By using swap functionality, you agree with provider’s %1$s and %2$s. More providers will be available soon Provider Best rate @@ -504,6 +507,7 @@ Create New Wallet Order Tangem Scan Tangem + Do you want to allow “Tangem” to use biometric authentication? To confirm your identity and open the app to %s On %s network Are you sure you want to exit the access code creation process? @@ -800,6 +804,7 @@ Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup. Save your wallet Creating a backup + Use biometrics Read more about seed phrase @@ -853,6 +858,7 @@ Access code restore Identical cards Access code + All offers Available with %s Providers facilitate transactions Search by country @@ -860,13 +866,20 @@ Other currencies Popular Fiats Search by currency + Instant By using onramp functionality, you agree with provider’s %1$s and %2$s The purchase amount should be no more than %s The amount to buy must be at least %s No available providers for this currency + Fastest Pay with + Payment method Available up to %s Available from %s + %d providers + Providers + Recently used + Recommended You will be able to complete your transaction on the third-party provider, %s Redirecting to %s... Our services are not available in this country @@ -875,6 +888,12 @@ Please select the correct country to ensure accurate payment options and services. Settings You can close this screen and check the transaction status on the token details screen. + + up to %d day + up to %d days + + %s min + You get You can close this screen and check the transaction status on the token details screen. Via You will pay @@ -1043,7 +1062,7 @@ Sending... Tap any field to change it Send %s - You are sending **%1$s** including a network fee of %2$s + You are sending **%1$s** including a network fee of %2$s. You are sending **%1$s** and %2$s You are sending **%1$s** network fee will be covered by using %1$s energy @@ -1668,4 +1687,68 @@ No, send all Reduce by %s XTZ To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ + Give Approve + Something went wrong with your previous approval, so we need a new one. Choose how you’d like to proceed. + Approve nedeed + The fee will be taken out, and your assets will be lent again. + To continue earning, approval is required. + Confirm approval + Text about your money in balance + Your %s is deposited in Aave + Earn %s% + Available + Current APY + My Funds + Your %1$s is now deposited in Aave and earning interest. You hold a%2$s token, which represents your balance and grows over time. When you top up, funds go to Aave to earn interest, minus a transaction fee. + Read more + Active + Stop earning + Earn + Total earnings + Automatic + Transfers to Aave + Text explaining that your savings are located in the protocol + Explore Aave + Explore default + Your %s is deposited in Aave + This is the current supply fee on %s. The live cost will be shown on the Receive Screen. + Current fee + All future %s top-ups will be supplied to Aave automatically, with the transaction fee deducted. + If network fees rise above maximum fee, the transaction won’t go through until they decrease. You can change this limit later. + Maximum fee + Fee policy + Write description here. In one, two or three lines will be awesome. + Some token approve needed + Every top-up of your account will be lended to Aave automatically. + Your balance works automatically + Send, swap, or sell your funds instantly, anytime you want. + Cash out instantly + How it works? + Aave is trusted by millions worldwide. Total lended value is $10.4B. + Decentralized and self-custodial + By using service, you agree with provider + Maximize your savings + Aave • Variable Interest Rate + Aave + Avg %s + Last year returns + Current interest rate is always variable and automatically computed by AAVE on-chain smart-contract based on real-time supply and demand. + Interest rate is variable + When you top up, your funds will be automatically sent to Aave to start earning interest. A small fee equal to %s will be deducted to cover the transaction. + Start earning + Your %s will be supplied to Aave and will stay instantly available + See fee policy + Network fee + Your next deposits will be automatically supplied to Aave. + Start earning + Turning off will withdraw your funds from Aave, return them to %s in your wallet, and stop earning rewards. + The network fee will be deducted from the amount you withdraw. + Stop earning + APY + Make your money work — earn interest on your balance. + Earning on your balance + Processing your deposit + Earn %s% per year + Deposit some %1$s %2$s to cover the network fee for transactions + Unable to cover %s fee diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index e27fce119a..5412605960 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -29,15 +29,15 @@ internal class DefaultSettingsRepository( private val userCountryFlow = MutableStateFlow(value = null) - override suspend fun shouldShowSaveUserWalletScreen(): Boolean { + override suspend fun shouldShowAskBiometry(): Boolean { return appPreferencesStore.getSyncOrDefault( - key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, + key = PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY, default = true, ) } - override suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) { - appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, value = value) + override suspend fun setShouldShowAskBiometry(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY, value = value) } override suspend fun isWalletScrollPreviewEnabled(): Boolean { diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetAskBiometryShownUseCase.kt similarity index 71% rename from domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt rename to domain/settings/src/main/java/com/tangem/domain/settings/SetAskBiometryShownUseCase.kt index 593b82a683..760ec572c6 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetAskBiometryShownUseCase.kt @@ -3,13 +3,13 @@ package com.tangem.domain.settings import arrow.core.Either import com.tangem.domain.settings.repositories.SettingsRepository -class SetSaveWalletScreenShownUseCase( +class SetAskBiometryShownUseCase( private val settingsRepository: SettingsRepository, ) { suspend operator fun invoke(): Either { return Either.catch { - settingsRepository.setShouldShowSaveUserWalletScreen(value = false) + settingsRepository.setShouldShowAskBiometry(value = false) } } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowAskBiometryUseCase.kt similarity index 57% rename from domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt rename to domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowAskBiometryUseCase.kt index 872f62c157..0cab3f56d4 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowAskBiometryUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.settings import com.tangem.domain.settings.repositories.SettingsRepository -class ShouldShowSaveWalletScreenUseCase(private val settingsRepository: SettingsRepository) { +class ShouldShowAskBiometryUseCase(private val settingsRepository: SettingsRepository) { - suspend operator fun invoke(): Boolean = settingsRepository.shouldShowSaveUserWalletScreen() + suspend operator fun invoke(): Boolean = settingsRepository.shouldShowAskBiometry() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index e39efd87a2..0a8f854790 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -7,9 +7,9 @@ import kotlinx.coroutines.flow.StateFlow @Suppress("TooManyFunctions") interface SettingsRepository { - suspend fun shouldShowSaveUserWalletScreen(): Boolean + suspend fun shouldShowAskBiometry(): Boolean - suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) + suspend fun setShouldShowAskBiometry(value: Boolean) suspend fun isWalletScrollPreviewEnabled(): Boolean 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 195b2c9069..0d53779766 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 @@ -15,7 +15,7 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase +import com.tangem.domain.settings.SetAskBiometryShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase @@ -39,7 +39,7 @@ import javax.inject.Inject internal class AskBiometryModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, - private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, + private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase, private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, @@ -66,7 +66,7 @@ internal class AskBiometryModel @Inject constructor( init { modelScope.launch { - setSaveWalletScreenShownUseCase() + setAskBiometryShownUseCase() } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index bfdc9b284b..9ced337ddf 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, - authMode = false, + isAuthMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 2db69f48ee..a3d0e3c9d7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -5,12 +5,20 @@ 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.decompose.ui.UiMessageSender +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.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.SetAskBiometryShownUseCase +import com.tangem.domain.settings.ShouldShowAskBiometryUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM +import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.UnlockHotWallet @@ -19,9 +27,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class AccessCodeModel @Inject constructor( @@ -31,6 +41,10 @@ internal class AccessCodeModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val walletsRepository: WalletsRepository, private val tangemHotSdk: TangemHotSdk, + private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase, + private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -87,6 +101,8 @@ internal class AccessCodeModel @Inject constructor( auth = HotAuth.Password(accessCode.toCharArray()), ) + tryToAskForBiometry() + if (walletsRepository.requireAccessCode().not()) { updatedHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( @@ -127,4 +143,49 @@ internal class AccessCodeModel @Inject constructor( } } } + + private suspend fun tryToAskForBiometry() { + if (!shouldAskForBiometry()) return + + suspendCancellableCoroutine { continuation -> + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.common_attention), + message = resourceReference(R.string.hot_access_code_set_biometric_ask), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_allow), + onClick = { + modelScope.launch { + setAskBiometryShownUseCase() + walletsRepository.setUseBiometricAuthentication(true) + walletsRepository.setRequireAccessCode(false) + continuation.resumeWith(Result.success(Unit)) + } + }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.save_user_wallet_agreement_dont_allow), + onClick = { + modelScope.launch { + setAskBiometryShownUseCase() + continuation.resumeWith(Result.success(Unit)) + } + }, + ), + onDismissRequest = { + modelScope.launch { + continuation.resumeWith(Result.success(Unit)) + } + }, + ), + ) + } + } + + private suspend fun shouldAskForBiometry(): Boolean { + val canUseBiometry = canUseBiometryUseCase() + val shouldShowAskBiometry = shouldShowAskBiometryUseCase() + + return canUseBiometry && shouldShowAskBiometry + } } \ No newline at end of file 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 e1aca5db5e..0ac07b8a3e 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 @@ -149,7 +149,7 @@ internal class OnboardingEntryModel @Inject constructor( doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated, ) { modelScope.launch { - if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { + if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) { doIfVisa { analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened) } 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 5c62c61337..e05be61546 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 @@ -19,8 +19,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog @@ -37,7 +37,10 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.StringsSigns import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -52,8 +55,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, private val saveWalletUseCase: SaveWalletUseCase, + private val getUserWalletsUseCase: GetWalletsUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -79,8 +82,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( // sets proper artwork state for initial step // (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY]) when (getInitialStep()) { - MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ - } + MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ } MultiWalletFinalizeUM.Step.BackupDevice1 -> { onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) } @@ -245,7 +247,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( userWalletCreated } OnboardingMultiWalletComponent.Mode.AddBackup -> { - val userWallet = userWalletsListManager.userWallets.first() + val userWallet = getUserWalletsUseCase.invokeSync() .firstOrNull { it is UserWallet.Cold && it.scanResponse.primaryCard?.cardId == scanResponse.primaryCard?.cardId diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 674541796f..97868f231b 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -14,7 +14,7 @@ interface UserWalletsFetcher { fun create( messageSender: UiMessageSender, onlyMultiCurrency: Boolean, - authMode: Boolean, + isAuthMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): UserWalletsFetcher } 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 b72ac8cf5c..6924fc073a 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,6 +10,7 @@ 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.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -35,6 +36,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener @@ -58,7 +60,7 @@ internal class WalletModel @Inject constructor( private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val setWalletFirstTimeUsageUseCase: SetWalletFirstTimeUsageUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, @@ -80,6 +82,8 @@ internal class WalletModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -137,7 +141,7 @@ internal class WalletModel @Inject constructor( modelScope.launch(dispatchers.main) { withContext(dispatchers.io) { delay(timeMillis = 1_800) } - if (isShowSaveWalletScreenEnabled()) { + if (shouldShowAskBiometryBottomSheet()) { innerWalletRouter.dialogNavigation.activate( configuration = WalletDialogConfig.AskForBiometry, ) @@ -159,8 +163,15 @@ internal class WalletModel @Inject constructor( } } - private suspend fun isShowSaveWalletScreenEnabled(): Boolean { - return innerWalletRouter.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase() + private suspend fun shouldShowAskBiometryBottomSheet(): Boolean { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && + innerWalletRouter.isWalletLastScreen() && + shouldShowAskBiometryUseCase() && + canUseBiometryUseCase() + } else { + innerWalletRouter.isWalletLastScreen() && shouldShowAskBiometryUseCase() && canUseBiometryUseCase() + } } private fun subscribeToUserWalletsUpdates() = channelFlow { 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 5c27b29b42..562c8a99a8 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 @@ -76,18 +76,19 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletNameChanged(state, wallets) -> { getRenameWalletsAction(state, wallets) } - isAnyHotWalletBackedUp(state, wallets) -> { + isAnyHotWalletBackedUpChange(state, wallets) -> { getHotWalletsBackedUpAction(state, wallets) } else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) } } - private fun isAnyHotWalletBackedUp(state: WalletScreenState, wallets: List): Boolean { + private fun isAnyHotWalletBackedUpChange(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() + val walletsToUpdate = wallets.filter { + it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId) + } + return walletsToUpdate.isNotEmpty() } private fun isWalletsCountChanged(state: WalletScreenState, wallets: List): Boolean { @@ -163,8 +164,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( val incompleteActivationWalletIds = state.incompleteActivationWalletIds() val walletsToUpdate = wallets.filter { - it is UserWallet.Hot && it.backedUp && - incompleteActivationWalletIds.contains(it.walletId) + it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId) } return Action.ReloadWarningsForWallets(walletsToUpdate) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index 1725952db7..a8c3a91d43 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach internal class MultiWalletWarningsSubscriber( @@ -31,6 +32,11 @@ internal class MultiWalletWarningsSubscriber( .onEach { warnings -> val displayedState = stateHolder.getWalletState(userWallet.walletId) + // Wait until the wallet appears in the list + stateHolder.uiState.first { + it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } + } + stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( 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 6a310e5c31..1b4e9c358f 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 @@ -43,7 +43,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, - @Assisted("authMode") private val authMode: Boolean, + @Assisted("isAuthMode") private val isAuthMode: Boolean, private val userWalletImageFetcher: UserWalletImageFetcher, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { @@ -55,7 +55,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> val uiModels = UserWalletItemUMConverter( onClick = onWalletClick, - authMode = authMode, + isAuthMode = isAuthMode, ).convertList(wallets) .toImmutableList() @@ -64,7 +64,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( combine( flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), - flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), + flow3 = getTotalBalanceFlow(wallets), flow4 = userWalletImageFetcher.walletsImage(wallets, ArtworkSize.SMALL), ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> createUiModels( @@ -88,6 +88,19 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( } .flowOn(dispatchers.default) + private fun getTotalBalanceFlow( + wallets: List, + ): Flow>> { + val walletIds = wallets.map(UserWallet::walletId) + + return if (isAuthMode) { + // We should not load balances in auth mode + flowOf(Lce.Loading(walletIds.associateWith { TotalFiatBalance.Loading })) + } else { + getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged() + } + } + private fun createUiModels( wallets: List, maybeAppCurrency: Either, @@ -117,7 +130,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], - authMode = authMode, + isAuthMode = isAuthMode, ) .convert(userWallet) } @@ -136,7 +149,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( override fun create( messageSender: UiMessageSender, @Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean, - @Assisted("authMode") authMode: Boolean, + @Assisted("isAuthMode") isAuthMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index 727e85c485..dc19a55e4e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -29,7 +29,7 @@ internal class WcUserWalletsFetcher( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = true, - authMode = false, + isAuthMode = false, onWalletClick = { onWalletSelected(it) }, ) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 3baa86b20e..d1128d02f2 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -55,7 +55,7 @@ internal class WelcomeModel @Inject constructor( private val walletsFetcher = userWalletsFetcherFactory.create( messageSender = uiMessageSender, onlyMultiCurrency = false, - authMode = true, + isAuthMode = true, onWalletClick = { walletId -> modelScope.launch { val userWallets = userWalletsListRepository.userWalletsSync() From 96ce2fc718e3bbeab28d59f11ff31b8ce1b6699a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Sep 2025 14:58:00 +0200 Subject: [PATCH 55/69] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 34 +- core/res/src/main/res/values/strings.xml | 5 +- .../src/main/res/drawable/ic_best_rate_16.xml | 9 + .../src/main/res/drawable/ic_fastest_16.xml | 10 + .../tangem/domain/onramp/model/OnrampOffer.kt | 5 +- .../onramp/GetOnrampAllOffersUseCase.kt | 5 +- .../domain/onramp/GetOnrampOffersUseCase.kt | 46 ++- .../domain/onramp/utils/OnrampOfferUtils.kt | 8 + .../onramp/alloffers/AllOffersComponent.kt | 17 + .../alloffers/DefaultAllOffersComponent.kt | 31 ++ .../di/AllOffersComponentModelModule.kt | 20 + .../alloffers/di/AllOffersComponentModule.kt | 18 + .../onramp/alloffers/model/AllOffersModel.kt | 19 + .../mainv2/DefaultOnrampV2MainComponent.kt | 10 + .../DefaultOnrampV2MainFeatureToggle.kt | 2 +- .../mainv2/entity/OnrampOfferBlockUM.kt | 23 +- .../entity/OnrampV2MainBottomSheetConfig.kt | 3 + .../factory/OnrampOffersStateFactory.kt | 106 ++++++ .../model/OnrampV2MainComponentModel.kt | 38 +- .../onramp/mainv2/ui/OnrampOffersContent.kt | 357 +++++++++++++++++- .../onramp/mainv2/ui/OnrampV2AmountContent.kt | 13 +- .../onramp/root/DefaultOnrampComponent.kt | 62 ++- 22 files changed, 784 insertions(+), 57 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_best_rate_16.xml create mode 100644 core/ui/src/main/res/drawable/ic_fastest_16.xml create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModelModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt 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 f5c4fda538..18f3540843 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 @@ -1,11 +1,7 @@ package com.tangem.tap.di.domain -import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.domain.onramp.* -import com.tangem.domain.onramp.repositories.HotCryptoRepository -import com.tangem.domain.onramp.repositories.OnrampErrorResolver -import com.tangem.domain.onramp.repositories.OnrampRepository -import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.onramp.repositories.* import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides @@ -237,4 +233,32 @@ internal object OnrampDomainModule { fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase { return GetLegacyTopUpUrlUseCase(legacyTopUpRepository) } + + @Provides + @Singleton + fun provideGetOnrampAllOffersUseCase( + onrampRepository: OnrampRepository, + onrampErrorResolver: OnrampErrorResolver, + settingsRepository: SettingsRepository, + ): GetOnrampAllOffersUseCase { + return GetOnrampAllOffersUseCase( + onrampRepository = onrampRepository, + errorResolver = onrampErrorResolver, + settingsRepository = settingsRepository, + ) + } + + @Provides + @Singleton + fun provideGetOnrampOffersUseCase( + onrampRepository: OnrampRepository, + onrampErrorResolver: OnrampErrorResolver, + onrampTransactionRepository: OnrampTransactionRepository, + ): GetOnrampOffersUseCase { + return GetOnrampOffersUseCase( + onrampRepository = onrampRepository, + errorResolver = onrampErrorResolver, + onrampTransactionRepository = onrampTransactionRepository, + ) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7d747732ea..b475fe441f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -876,7 +876,10 @@ Payment method Available up to %s Available from %s - %d providers + + %d provider + %d providers + Providers Recently used Recommended diff --git a/core/ui/src/main/res/drawable/ic_best_rate_16.xml b/core/ui/src/main/res/drawable/ic_best_rate_16.xml new file mode 100644 index 0000000000..7e36b66936 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_best_rate_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_fastest_16.xml b/core/ui/src/main/res/drawable/ic_fastest_16.xml new file mode 100644 index 0000000000..072444ff1d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fastest_16.xml @@ -0,0 +1,10 @@ + + + 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 index e116e55735..56700b01f4 100644 --- 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 @@ -1,13 +1,16 @@ package com.tangem.domain.onramp.model +import java.math.BigDecimal + data class OnrampOffersBlock( val category: OnrampOfferCategory, val offers: List, - val isVisible: Boolean = offers.isNotEmpty(), + val hasMoreOffers: Boolean, ) data class OnrampOffer( val quote: OnrampQuote, + val rateDif: BigDecimal?, val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default, ) 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 index 6cc1247867..f2f787e182 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt @@ -12,6 +12,7 @@ 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.onramp.utils.calculateRateDif import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -38,6 +39,7 @@ class GetOnrampAllOffersUseCase( val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val bestRate = overallBestRateQuote?.toAmount?.value val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } @@ -48,7 +50,8 @@ class GetOnrampAllOffersUseCase( } else { OnrampOfferAdvantages.Default } - OnrampOffer(quote = quote, advantages = advantages) + val rateDif = calculateRateDif(quote.toAmount.value, bestRate) + OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages) } val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } 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 index 29041336a9..1632abd3cc 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -11,6 +11,7 @@ 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 com.tangem.domain.onramp.utils.calculateRateDif import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -43,8 +44,12 @@ class GetOnrampOffersUseCase( val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() + val bestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val bestRate = bestRateQuote?.toAmount?.value + val offers = validQuotes.map { quote -> - OnrampOffer(quote = quote) + val rateDif = calculateRateDif(quote.toAmount.value, bestRate) + OnrampOffer(quote = quote, rateDif = rateDif) } val recentOffer = findRecentOffer(offers, transactions) @@ -107,6 +112,15 @@ class GetOnrampOffersUseCase( fastestOffer: OnrampOffer?, allOffers: List, ): List { + val recommendedOffers = buildRecommendedOffers( + recentOffer = recentOffer, + bestRateOffer = bestRateOffer, + fastestOffer = fastestOffer, + ) + + val shownOffersCount = (if (recentOffer != null) 1 else 0) + recommendedOffers.size + val hasMoreOffers = allOffers.size > shownOffersCount + return buildList { if (recentOffer != null) { add( @@ -119,23 +133,20 @@ class GetOnrampOffersUseCase( bestRateOffer, fastestOffer, ), + rateDif = if (bestRateOffer != null) recentOffer.rateDif else null, ), ), + hasMoreOffers = false, ), ) } - val recommendedOffers = buildRecommendedOffers( - recentOffer = recentOffer, - bestRateOffer = bestRateOffer, - fastestOffer = fastestOffer, - ) - if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) { add( OnrampOffersBlock( category = OnrampOfferCategory.Recommended, offers = recommendedOffers, + hasMoreOffers = hasMoreOffers, ), ) } @@ -167,17 +178,32 @@ class GetOnrampOffersUseCase( return buildList { if (isSameOffer(bestRateOffer, fastestOffer)) { bestRateOffer?.let { offer -> - add(offer.copy(advantages = OnrampOfferAdvantages.BestRate)) + add( + offer.copy( + advantages = OnrampOfferAdvantages.BestRate, + rateDif = null, + ), + ) } } else { if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) { - add(bestRateOffer.copy(advantages = OnrampOfferAdvantages.BestRate)) + add( + bestRateOffer.copy( + advantages = OnrampOfferAdvantages.BestRate, + rateDif = null, + ), + ) } if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) && !isSameOffer(fastestOffer, bestRateOffer) ) { - add(fastestOffer.copy(advantages = OnrampOfferAdvantages.Fastest)) + add( + fastestOffer.copy( + advantages = OnrampOfferAdvantages.Fastest, + rateDif = if (bestRateOffer != null) fastestOffer.rateDif else null, + ), + ) } } } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt new file mode 100644 index 0000000000..0b3625d494 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.onramp.utils + +import java.math.BigDecimal + +internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? { + if (bestRate == null) return null + return BigDecimal.ONE - currentTokenRate / bestRate +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt new file mode 100644 index 0000000000..7c2de6da52 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.alloffers + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet + +internal interface AllOffersComponent : ComposableBottomSheetComponent { + + data class Params( + val userWallet: UserWallet, + val cryptoCurrency: CryptoCurrency, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt new file mode 100644 index 0000000000..02adaaa504 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.alloffers + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.alloffers.model.AllOffersModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAllOffersComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: AllOffersComponent.Params, +) : AllOffersComponent, AppComponentContext by context { + + private val model: AllOffersModel = getOrCreateModel(params) + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + // TODO to be continued in [REDACTED_TASK_KEY] + } + + @AssistedFactory + interface Factory : AllOffersComponent.Factory { + override fun create(context: AppComponentContext, params: AllOffersComponent.Params): DefaultAllOffersComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModelModule.kt new file mode 100644 index 0000000000..e709ab73d6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.alloffers.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.alloffers.model.AllOffersModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface AllOffersComponentModelModule { + + @Binds + @IntoMap + @ClassKey(AllOffersModel::class) + fun bindAllOffersModel(model: AllOffersModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModule.kt new file mode 100644 index 0000000000..3979f343c2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/di/AllOffersComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.alloffers.di + +import com.tangem.features.onramp.alloffers.AllOffersComponent +import com.tangem.features.onramp.alloffers.DefaultAllOffersComponent +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 AllOffersComponentModule { + + @Binds + @Singleton + fun bindAllOffersComponentFactory(factory: DefaultAllOffersComponent.Factory): AllOffersComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt new file mode 100644 index 0000000000..3280c17cb9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.alloffers.model + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.onramp.alloffers.AllOffersComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +internal class AllOffersModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: AllOffersComponent.Params = paramsContainer.require() + + fun dismiss() { + params.onDismiss() + } +} \ No newline at end of file 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 index a651416d7f..2487bf37b1 100644 --- 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 @@ -12,6 +12,7 @@ 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.alloffers.AllOffersComponent import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel @@ -26,6 +27,7 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor( @Assisted private val params: OnrampV2MainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, + private val allOffersComponentFactory: AllOffersComponent.Factory, ) : OnrampV2MainComponent, AppComponentContext by appComponentContext { private val model: OnrampV2MainComponentModel = getOrCreateModel(params) @@ -67,6 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor( onDismiss = model.bottomSheetNavigation::dismiss, ), ) + OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( + context = childByContext(componentContext), + params = AllOffersComponent.Params( + userWallet = model.userWallet, + cryptoCurrency = params.cryptoCurrency, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt index 256bead28d..815fa5060b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt @@ -6,5 +6,5 @@ class DefaultOnrampV2MainFeatureToggle( private val featureTogglesManager: FeatureTogglesManager, ) : OnrampV2MainFeatureToggle { override val isOnrampNewMainEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED") + get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED") } \ 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 index 583cc962bb..5e41b49346 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.features.onramp.mainv2.entity import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.collections.immutable.ImmutableList @@ -20,22 +21,32 @@ internal sealed interface OnrampOffersBlockUM { data class Content( override val isBlockVisible: Boolean, - val offers: ImmutableList, + val recentOffer: OnrampOfferUM?, + val recommended: ImmutableList, + val onrampAllOffersButtonConfig: OnrampAllOffersButtonConfig?, ) : OnrampOffersBlockUM } internal data class OnrampOfferUM( - val category: OnrampOfferCategory, - val advantages: OnrampOfferAdvantages, + val category: OnrampOfferCategoryUM, + val advantages: OnrampOfferAdvantagesUM, val paymentMethod: OnrampPaymentMethod, val providerId: String, val providerName: String, + val rate: String, + val diff: TextReference?, + val onBuyClicked: () -> Unit, ) -internal enum class OnrampOfferCategory { +internal enum class OnrampOfferCategoryUM { RecentlyUsed, Recommended } -internal enum class OnrampOfferAdvantages { +internal enum class OnrampOfferAdvantagesUM { Default, BestRate, Fastest -} \ No newline at end of file +} + +internal data class OnrampAllOffersButtonConfig( + val title: TextReference, + 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 index dcb21bbf8a..d9e6c47a2d 100644 --- 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 @@ -10,4 +10,7 @@ sealed interface OnrampV2MainBottomSheetConfig { @Serializable data object CurrenciesList : OnrampV2MainBottomSheetConfig + + @Serializable + data object AllOffers : OnrampV2MainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt new file mode 100644 index 0000000000..377c5987d4 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt @@ -0,0 +1,106 @@ +package com.tangem.features.onramp.mainv2.entity.factory + +import com.tangem.core.ui.extensions.TextReference +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.format.bigdecimal.percent +import com.tangem.domain.onramp.model.* +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 com.tangem.utils.StringsSigns.MINUS +import kotlinx.collections.immutable.toPersistentList + +internal class OnrampOffersStateFactory( + private val currentStateProvider: Provider, + private val onrampIntents: OnrampIntents, +) { + + fun getOnShowOffersState(offers: List): OnrampV2MainComponentUM { + val currentState = currentStateProvider.invoke() + return when (currentState) { + is OnrampV2MainComponentUM.Content -> { + currentState.copy( + offersBlockState = mapOnrampOffersBlockToUM(offers), + ) + } + is OnrampV2MainComponentUM.InitialLoading -> { + currentState + } + } + } + + private fun mapOnrampOffersBlockToUM(offersBlocks: List): OnrampOffersBlockUM.Content { + val allOffersUM = mutableListOf() + offersBlocks.map { block -> + block.offers.forEach { offer -> + when (val currentQuote = offer.quote) { + is OnrampQuote.AmountError, + is OnrampQuote.Error, + -> Unit + is OnrampQuote.Data -> { + allOffersUM.add( + OnrampOfferUM( + category = mapOfferCategoryDTOtoUM(block.category), + advantages = mapOfferAdvantagesDTOtoUM(offer.advantages), + paymentMethod = currentQuote.paymentMethod, + providerId = currentQuote.provider.id, + providerName = currentQuote.provider.info.name, + rate = currentQuote.toAmount.value.format { + crypto( + symbol = currentQuote.toAmount.symbol, + decimals = currentQuote.toAmount.decimals, + ) + }, + diff = offer.rateDif?.let { diff -> + stringReference("$MINUS${diff.format { percent() }}") + }, + onBuyClicked = { + onrampIntents.onBuyClick( + quote = OnrampProviderWithQuote.Data( + provider = offer.quote.provider, + paymentMethod = currentQuote.paymentMethod, + toAmount = currentQuote.toAmount, + fromAmount = currentQuote.fromAmount, + ), + ) + }, + ), + ) + } + } + } + } + + return OnrampOffersBlockUM.Content( + isBlockVisible = false, + recentOffer = allOffersUM.firstOrNull { it.category == OnrampOfferCategoryUM.RecentlyUsed }, + recommended = allOffersUM.filter { it.category == OnrampOfferCategoryUM.Recommended }.toPersistentList(), + onrampAllOffersButtonConfig = if (offersBlocks.any { it.hasMoreOffers }) { + OnrampAllOffersButtonConfig( + title = TextReference.Res(R.string.onramp_all_offers_button_title), + onClick = { onrampIntents.openProviders() }, + ) + } else { + null + }, + ) + } + + private fun mapOfferCategoryDTOtoUM(category: OnrampOfferCategory): OnrampOfferCategoryUM { + return when (category) { + OnrampOfferCategory.Recent -> OnrampOfferCategoryUM.RecentlyUsed + OnrampOfferCategory.Recommended -> OnrampOfferCategoryUM.Recommended + } + } + + private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM { + return when (advantages) { + OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default + OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate + OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest + } + } +} \ 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 index 8d6615a675..d83a94d1f0 100644 --- 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 @@ -20,6 +20,7 @@ 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.OnrampOffersStateFactory 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 @@ -32,7 +33,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class OnrampV2MainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -44,6 +45,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, private val amountInputManager: InputManager, + private val getOnrampOffersUseCase: GetOnrampOffersUseCase, paramsContainer: ParamsContainer, getWalletsUseCase: GetWalletsUseCase, ) : Model(), OnrampIntents { @@ -58,6 +60,13 @@ internal class OnrampV2MainComponentModel @Inject constructor( OnrampAmountButtonUMStateFactory() } + private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampOffersStateFactory( + currentStateProvider = Provider { _state.value }, + onrampIntents = this, + ) + } + private val stateFactory = OnrampV2StateFactory( currentStateProvider = Provider { _state.value }, cryptoCurrency = params.cryptoCurrency, @@ -94,6 +103,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( subscribeToAmountChanges() subscribeToCountryAndCurrencyUpdates() subscribeToQuotesUpdate() + subscribeOnOffers() } override fun onDestroy() { @@ -117,11 +127,19 @@ internal class OnrampV2MainComponentModel @Inject constructor( } override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { - // TODO in [REDACTED_TASK_KEY] to be continued + val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return + analyticsEventHandler.send( + OnrampAnalyticsEvent.OnBuyClick( + providerName = quote.provider.info.name, + currency = currentContentState.amountBlockState.currencyUM.code, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + params.openRedirectPage(quote) } override fun openProviders() { - // TODO in [REDACTED_TASK_KEY] to be continued + bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers) } override fun onRefresh() { @@ -172,6 +190,20 @@ internal class OnrampV2MainComponentModel @Inject constructor( router.pop() } + private fun subscribeOnOffers() = modelScope.launch { + getOnrampOffersUseCase.invoke( + userWalletId = userWallet.walletId, + cryptoCurrencyId = params.cryptoCurrency.id, + ).collectLatest { maybeOffers -> + maybeOffers.fold( + ifLeft = ::handleOnrampError, + ifRight = { offers -> + _state.update { onrampOffersStateFactory.getOnShowOffersState(offers) } + }, + ) + } + } + private fun subscribeToAmountChanges() = modelScope.launch { amountInputManager.query .filter(String::isNotEmpty) 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 index 6b35933d9e..1fc602096c 100644 --- 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 @@ -1,13 +1,42 @@ package com.tangem.features.onramp.mainv2.ui +import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodType +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM +import kotlinx.collections.immutable.persistentListOf @Composable internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { @@ -23,10 +52,330 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { ), label = "Offers block animation", ) { - Text( - text = "Some offers", - style = TangemTheme.typography.head, + if (state is OnrampOffersBlockUM.Content) { + Column(modifier = Modifier.fillMaxWidth()) { + state.recentOffer?.let { recentOffer -> + Column { + Text( + modifier = Modifier.padding(start = 12.dp), + text = stringResourceSafe(R.string.onramp_recently_used_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + SpacerH(8.dp) + + Offer(recentOffer) + + SpacerH(16.dp) + } + } + + Text( + modifier = Modifier.padding(start = 12.dp), + text = stringResourceSafe(R.string.onramp_recommended_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + SpacerH(8.dp) + + state.recommended.fastForEach { + Offer(it) + SpacerH(8.dp) + } + + state.onrampAllOffersButtonConfig?.let { + SpacerH(12.dp) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = it.title.resolveReference(), + onClick = it.onClick, + ) + } + } + } + } +} + +@Composable +private fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(14.dp), + ) + .padding(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + OfferHeader(advantage = onrampOfferUM.advantages) + RateBlock(rate = onrampOfferUM.rate, diff = onrampOfferUM.diff) + } + SpacerWMax() + SecondaryButton( + size = TangemButtonSize.RoundedAction, + text = stringResourceSafe(R.string.common_buy), + onClick = onrampOfferUM.onBuyClicked, + ) + } + SpacerH(10.dp) + HorizontalDivider(color = TangemTheme.colors.stroke.primary) + SpacerH(12.dp) + PaymentBlockInOffer( + paymentMethod = onrampOfferUM.paymentMethod, + providerName = onrampOfferUM.providerName, ) - // TODO in [REDACTED_TASK_KEY] to be continued + } +} + +@Composable +private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) { + when (advantage) { + OnrampOfferAdvantagesUM.Default -> { + Text( + text = stringResourceSafe(R.string.onramp_title_you_get), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + OnrampOfferAdvantagesUM.BestRate -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_16), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + ) + } + } + OnrampOfferAdvantagesUM.Fastest -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_fastest_16), + tint = TangemTheme.colors.icon.attention, + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.onramp_offer_type_fastet), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.attention, + ) + } + } + } +} + +@Composable +private fun RateBlock(rate: String, diff: TextReference?) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = rate, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + diff?.let { + Text( + modifier = Modifier + .background( + color = TangemTheme.colors.text.warning.copy(alpha = 0.1f), + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 4.dp), + text = it.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.warning, + ) + } + } +} + +@Composable +private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + + SpacerW(2.dp) + + TimingBlock(speed = paymentMethod.type.getProcessingSpeed()) + + SpacerW(6.dp) + + DrawDot(color = TangemTheme.colors.text.tertiary) + + SpacerW(6.dp) + + Text( + text = providerName, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + + SpacerWMax() + + Text( + text = stringResourceSafe(R.string.onramp_pay_with), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + + SpacerW(4.dp) + + SubcomposeAsyncImage( + modifier = Modifier.sizeIn(maxWidth = 38.dp, maxHeight = 16.dp), + model = ImageRequest.Builder(context = LocalContext.current) + .data(paymentMethod.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { + TextShimmer( + style = TangemTheme.typography.body1, + modifier = Modifier.width(40.dp), + ) + }, + contentDescription = null, + ) + } +} + +@Composable +private fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) { + val timingText = when (speed) { + PaymentMethodType.PaymentSpeed.Instant -> { + stringResourceSafe(id = R.string.onramp_instant_status) + } + PaymentMethodType.PaymentSpeed.FewMin -> { + stringResourceSafe( + id = R.string.onramp_timing_minutes, + FEW_MINS_VALUE, + ) + } + PaymentMethodType.PaymentSpeed.FewDays -> { + pluralStringResourceSafe( + id = R.plurals.onramp_timing_days, + count = FEW_DAYS_VALUE, + FEW_DAYS_VALUE, + ) + } + PaymentMethodType.PaymentSpeed.PlentyDays -> { + pluralStringResourceSafe( + id = R.plurals.onramp_timing_days, + count = PLENTY_DAYS_VALUE, + PLENTY_DAYS_VALUE, + ) + } + PaymentMethodType.PaymentSpeed.Unknown -> "" + } + + Text( + text = timingText, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +fun DrawDot(color: Color) { + Spacer( + modifier = Modifier + .size(4.dp) + .drawWithCache { + val radius = size.minDimension / 2f + onDrawBehind { + drawCircle( + color = color, + radius = radius, + ) + } + }, + ) +} + +// will be hardcoded till server will be ready to provide this values +private const val FEW_MINS_VALUE = "3-5" +private const val FEW_DAYS_VALUE = 3 +private const val PLENTY_DAYS_VALUE = 5 + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OnrampOffersContentPreview() { + val state = OnrampOffersBlockUM.Content( + isBlockVisible = true, + recentOffer = OnrampOfferUM( + category = OnrampOfferCategoryUM.RecentlyUsed, + advantages = OnrampOfferAdvantagesUM.Default, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId3", + providerName = "Simplex", + rate = "0,00045334 BTC", + diff = stringReference("–27%"), + onBuyClicked = {}, + ), + recommended = persistentListOf( + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.BestRate, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId1", + providerName = "Simplex", + rate = "0,0245334 BTC", + diff = null, + onBuyClicked = {}, + ), + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.Fastest, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId2", + providerName = "Simplex", + rate = "0,00145334 BTC", + diff = stringReference("–0.07%"), + onBuyClicked = {}, + ), + ), + onrampAllOffersButtonConfig = null, + ) + TangemThemePreview { + OnrampOffersContent(state) } } \ 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 index 284e2e04cc..b0ea37b98b 100644 --- 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 @@ -1,5 +1,6 @@ package com.tangem.features.onramp.mainv2.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -51,6 +52,7 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif Column( modifier = modifier + .fillMaxWidth() .background( color = TangemTheme.colors.background.action, shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), @@ -61,16 +63,14 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif ) { OnrampHeaderTitle() - SpacerH(12.dp) - OnrampAmountField( amountField = state.amountBlockState.amountFieldModel, currencyCode = state.amountBlockState.currencyUM.code, ) - SpacerH(8.dp) - - OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + AnimatedVisibility(!state.offersBlockState.isBlockVisible) { + OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + } SpacerH(20.dp) @@ -115,7 +115,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin modifier = Modifier .focusRequester(requester) .padding( - top = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) 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 e9ee2b3295..b9ff5cb77a 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 @@ -17,6 +17,8 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.mainv2.OnrampV2MainComponent +import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent @@ -24,13 +26,15 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, + private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory, private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, + private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -66,24 +70,44 @@ internal class DefaultOnrampComponent @AssistedInject constructor( onBack = navigation::pop, ), ) - OnrampChild.Main -> onrampMainComponentFactory.create( - context = childByContext(componentContext), - params = OnrampMainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { - navigation.push( - OnrampChild.RedirectPage( - quote = it, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - launchSepa = params.launchSepa, - ), - ) + OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) { + onrampMainV2ComponentFactory.create( + context = childByContext(componentContext), + params = OnrampV2MainComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + openSettings = { navigation.push(OnrampChild.Settings) }, + source = params.source, + openRedirectPage = { + navigation.push( + OnrampChild.RedirectPage( + quote = it, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) + } else { + onrampMainComponentFactory.create( + context = childByContext(componentContext), + params = OnrampMainComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + openSettings = { navigation.push(OnrampChild.Settings) }, + source = params.source, + openRedirectPage = { + navigation.push( + OnrampChild.RedirectPage( + quote = it, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + launchSepa = params.launchSepa, + ), + ) + } is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( context = childByContext(componentContext), params = OnrampRedirectComponent.Params( From fd7bc522f23bd18f88258172a2c18040c91b6e15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Sep 2025 17:22:08 +0400 Subject: [PATCH 56/69] Updated on 2026-08-14 --- app/build.gradle.kts | 2 ++ app/src/main/AndroidManifest.xml | 10 ++++++ .../tangem/tap/routing/utils/ChildFactory.kt | 9 +++++ .../tap/routing/utils/DeepLinkFactory.kt | 3 ++ .../com/tangem/common/routing/AppRoute.kt | 5 +++ .../tangem/common/routing/DeepLinkRoute.kt | 4 +++ features/tangempay/onboarding/api/.gitignore | 1 + .../tangempay/onboarding/api/build.gradle.kts | 18 ++++++++++ .../TangemPayOnboardingComponent.kt | 13 +++++++ .../deeplink/OnboardVisaDeepLinkHandler.kt | 10 ++++++ features/tangempay/onboarding/impl/.gitignore | 1 + .../onboarding/impl/build.gradle.kts | 34 ++++++++++++++++++ .../DefaultTangemPayOnboardingComponent.kt | 35 +++++++++++++++++++ .../DefaultOnboardVisaDeepLinkHandler.kt | 23 ++++++++++++ .../tangempay/di/TangemPayDeeplinkModule.kt | 18 ++++++++++ .../di/TangemPayOnboardingFeatureModule.kt | 16 +++++++++ .../di/TangemPayOnboardingModelsModule.kt | 20 +++++++++++ .../model/TangemPayOnboardingModel.kt | 28 +++++++++++++++ .../tangempay/ui/TandemPayOnboardingScreen.kt | 31 ++++++++++++++++ .../ui/TangemPayOnboardingScreenState.kt | 3 ++ settings.gradle.kts | 6 +++- 21 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 features/tangempay/onboarding/api/.gitignore create mode 100644 features/tangempay/onboarding/api/build.gradle.kts create mode 100644 features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt create mode 100644 features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/deeplink/OnboardVisaDeepLinkHandler.kt create mode 100644 features/tangempay/onboarding/impl/.gitignore create mode 100644 features/tangempay/onboarding/impl/build.gradle.kts create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDeeplinkModule.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5fdb2e580..3458dfc6a4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -270,6 +270,8 @@ dependencies { implementation(projects.features.tangempay.details.impl) implementation(projects.features.tangempay.main.api) implementation(projects.features.tangempay.main.impl) + implementation(projects.features.tangempay.onboarding.api) + implementation(projects.features.tangempay.onboarding.impl) implementation(projects.features.tokenRecieve.api) implementation(projects.features.tokenRecieve.impl) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index baf6ae4211..87d3840efe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -264,6 +264,16 @@ android:host="promo" android:scheme="tangem" /> + + + + + + + + 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 9525c29fa0..653b45d4c1 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 @@ -55,6 +55,7 @@ import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent @@ -114,6 +115,7 @@ internal class ChildFactory @Inject constructor( private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, + private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -606,6 +608,13 @@ internal class ChildFactory @Inject constructor( componentFactory = tangemPayDetailsComponentFactory, ) } + is AppRoute.TangemPayOnboarding -> { + createComponentChild( + context = context, + params = TangemPayOnboardingComponent.Params(route.deeplink), + componentFactory = tangemPayOnboardingComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 3412cac097..45a9e27528 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -14,6 +14,7 @@ import com.tangem.features.onramp.deeplink.SellDeepLinkHandler import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler +import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler @@ -48,6 +49,7 @@ internal class DeepLinkFactory @Inject constructor( private val sellDeepLink: SellDeepLinkHandler.Factory, private val swapDeepLink: SwapDeepLinkHandler.Factory, private val promoDeepLink: PromoDeeplinkHandler.Factory, + private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -135,6 +137,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Swap.host -> swapDeepLink.create() DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri) DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri) else -> { Timber.i( """ 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 06cede4e5a..96fcdbc5fd 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 @@ -375,4 +375,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class TangemPayDetails(val userWalletId: UserWalletId) : AppRoute(path = "/tangem_pay_details") + + @Serializable + data class TangemPayOnboarding( + val deeplink: String, + ) : AppRoute(path = "/tangem_pay_onboarding/$deeplink") } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index 1fbffcb8a8..9b5ba24bfa 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -59,6 +59,10 @@ sealed class DeepLinkRoute { data object Promo : DeepLinkRoute() { override val host: String = "promo" } + + data object OnboardVisa : DeepLinkRoute() { + override val host: String = "onboard-visa" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/features/tangempay/onboarding/api/.gitignore b/features/tangempay/onboarding/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/onboarding/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/onboarding/api/build.gradle.kts b/features/tangempay/onboarding/api/build.gradle.kts new file mode 100644 index 0000000000..a52e5bbccd --- /dev/null +++ b/features/tangempay/onboarding/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.onboarding.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt new file mode 100644 index 0000000000..d2257362fb --- /dev/null +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayOnboardingComponent : ComposableContentComponent { + + data class Params( + val deeplink: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/deeplink/OnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/deeplink/OnboardVisaDeepLinkHandler.kt new file mode 100644 index 0000000000..eb9bd4ac28 --- /dev/null +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/deeplink/OnboardVisaDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.deeplink + +import android.net.Uri + +interface OnboardVisaDeepLinkHandler { + + interface Factory { + fun create(uri: Uri): OnboardVisaDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/.gitignore b/features/tangempay/onboarding/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/onboarding/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts new file mode 100644 index 0000000000..ab2de5b977 --- /dev/null +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.onboarding.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Common */ + implementation(projects.common.routing) + + /** Features api */ + implementation(projects.features.tangempay.onboarding.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt new file mode 100644 index 0000000000..c770c768d1 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tangempay.components + +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.tangempay.model.TangemPayOnboardingModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen + +internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: TangemPayOnboardingComponent.Params, +) : TangemPayOnboardingComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayOnboardingModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.screenState.collectAsStateWithLifecycle() + TandemPayOnboardingScreen(modifier = modifier, state = state) + } + + @AssistedFactory + interface Factory : TangemPayOnboardingComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayOnboardingComponent.Params, + ): DefaultTangemPayOnboardingComponent + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt new file mode 100644 index 0000000000..a843aade0a --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -0,0 +1,23 @@ +package com.tangem.features.tangempay.deeplink + +import android.net.Uri +import dagger.assisted.Assisted +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( + @Assisted uri: Uri, + appRouter: AppRouter, +) : OnboardVisaDeepLinkHandler { + + init { + appRouter.push(AppRoute.TangemPayOnboarding(uri.toString())) + } + + @AssistedFactory + interface Factory : OnboardVisaDeepLinkHandler.Factory { + override fun create(uri: Uri): DefaultOnboardVisaDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDeeplinkModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDeeplinkModule.kt new file mode 100644 index 0000000000..a1d13bcabc --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDeeplinkModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.deeplink.DefaultOnboardVisaDeepLinkHandler +import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler +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 TangemPayDeeplinkModule { + + @Binds + @Singleton + fun bindDeepLinkHandlerFactory(impl: DefaultOnboardVisaDeepLinkHandler.Factory): OnboardVisaDeepLinkHandler.Factory +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt new file mode 100644 index 0000000000..6e36c8215a --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt @@ -0,0 +1,16 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.components.DefaultTangemPayOnboardingComponent +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayOnboardingFeatureModule { + + @Binds + fun bindFactory(impl: DefaultTangemPayOnboardingComponent.Factory): TangemPayOnboardingComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt new file mode 100644 index 0000000000..b4b1892e56 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.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.TangemPayOnboardingModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface TangemPayOnboardingModelsModule { + + @Binds + @IntoMap + @ClassKey(TangemPayOnboardingModel::class) + fun bindModel(model: TangemPayOnboardingModel): Model +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt new file mode 100644 index 0000000000..02ca64323d --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -0,0 +1,28 @@ +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.model.ParamsContainer +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayOnboardingModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() + + private val _screenState: MutableStateFlow = MutableStateFlow(getInitState()) + val screenState = _screenState.asStateFlow() + + private fun getInitState() = TangemPayOnboardingScreenState +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt new file mode 100644 index 0000000000..008f2c8798 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt @@ -0,0 +1,31 @@ +package com.tangem.features.tangempay.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton + +@Suppress("UnusedPrivateMember") +@Composable +internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + topBar = { + AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), + onBackClick = {}, + text = "", + iconRes = R.drawable.ic_back_24, + ) + }, + content = { paddingValues -> + val contentModifier = Modifier + .padding(paddingValues) + .fillMaxSize() + }, + ) +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt new file mode 100644 index 0000000000..a0681a0db9 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt @@ -0,0 +1,3 @@ +package com.tangem.features.tangempay.ui + +internal object TangemPayOnboardingScreenState \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 19c6d1c875..99aee86df3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -272,6 +272,9 @@ include(":features:tangempay:main:impl") include(":features:tangempay:details:api") include(":features:tangempay:details:impl") +include(":features:tangempay:onboarding:api") +include(":features:tangempay:onboarding:impl") + include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") @@ -380,4 +383,5 @@ include(":data:blockaid") include(":data:swap") include(":data:express") include(":data:wallet-manager") -// endregion Data modules \ No newline at end of file +// endregion Data modules +include(":features:tangempay:onboarding") \ No newline at end of file From 194e750c933c508bbc4aca4f78b426ad61400334 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Sep 2025 18:54:08 +0400 Subject: [PATCH 57/69] Updated on 2026-08-14 --- app/build.gradle.kts | 1 - .../com/tangem/tap/ApplicationEntryPoint.kt | 6 +- .../main/java/com/tangem/tap/MainActivity.kt | 21 +- .../java/com/tangem/tap/TangemApplication.kt | 16 +- .../com/tangem/tap/common/DialogManager.kt | 89 +-- .../common/analytics/events/WalletConnect.kt | 120 --- .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../com/tangem/tap/di/IntentHandlingModule.kt | 5 - .../tap/domain/walletconnect/BnbHelper.kt | 77 -- .../tap/domain/walletconnect/EthSignHelper.kt | 31 - .../walletconnect/WalletConnectSdkHelper.kt | 529 -------------- .../app/TangemWcBlockchainHelper.kt | 119 --- .../app/WalletConnectEventsHandlerImpl.kt | 53 -- .../DefaultLegacyWalletConnectRepository.kt | 686 ------------------ .../DefaultWalletConnectSessionsRepository.kt | 57 -- .../di/WalletConnectInteractorModule.kt | 110 --- .../domain/LegacyWalletConnectRepository.kt | 36 - .../domain/WalletConnectEventsHandler.kt | 21 - .../domain/WalletConnectInteractor.kt | 457 ------------ .../domain/WcBlockchainHelper.kt | 20 - .../walletconnect2/domain/WcJrpcMethods.kt | 297 -------- .../domain/WcPreparedRequest.kt | 68 -- .../domain/WcSessionRequestConverter.kt | 184 ----- .../walletconnect2/domain/models/BnbData.kt | 10 - .../domain/models/EthTransactionData.kt | 15 - .../domain/models/NetworkNamespace.kt | 4 - .../domain/models/RequestData.kt | 8 - .../domain/models/WalletConnectError.kt | 31 - .../domain/models/WalletConnectEvents.kt | 32 - .../domain/models/WalletConnectSession.kt | 8 - .../models/binance/WcBinanceCancelOrder.kt | 35 - .../models/binance/WcBinanceTradeOrder.kt | 73 -- .../models/binance/WcBinanceTransferOrder.kt | 51 -- .../models/binance/WcBinanceTxConfirmParam.kt | 13 - .../domain/models/solana/SolanaSignMessage.kt | 13 - .../models/solana/SolanaTransactionRequest.kt | 14 - .../DefaultWalletConnectFeatureToggles.kt | 12 - .../tangem/tap/features/demo/DemoHelper.kt | 24 +- .../details/redux/DetailsMiddleware.kt | 2 +- .../walletconnect/WalletConnectAction.kt | 45 -- .../walletconnect/WalletConnectMiddleware.kt | 202 ------ .../walletconnect/WalletConnectReducer.kt | 26 - .../redux/walletconnect/WalletConnectState.kt | 173 ----- .../DefaultWalletConnectComponent.kt | 73 -- .../ui/walletconnect/WalletConnectModel.kt | 72 -- .../ui/walletconnect/WalletConnectScreen.kt | 169 ----- .../walletconnect/WalletConnectScreenState.kt | 15 - .../api/WalletConnectComponent.kt | 10 - .../di/WalletConnectFeatureModule.kt | 25 - .../dialogs/BnbTransactionDialog.kt | 55 -- .../dialogs/PersonalSignDialog.kt | 37 - .../dialogs/SessionProposalDialog.kt | 42 -- .../dialogs/SignTransactionDialog.kt | 30 - .../dialogs/SignTransactionsDialog.kt | 30 - .../dialogs/TransactionDialog.kt | 61 -- .../WalletConnectLinkIntentHandler.kt | 48 -- .../products/wallet/redux/BackupMiddleware.kt | 11 +- .../middlewares/TradeCryptoMiddleware.kt | 2 +- .../welcome/redux/WelcomeMiddleware.kt | 37 +- .../tap/proxy/redux/DaggerGraphAction.kt | 2 - .../tap/proxy/redux/DaggerGraphReducer.kt | 1 - .../tap/proxy/redux/DaggerGraphState.kt | 4 - .../tangem/tap/routing/utils/ChildFactory.kt | 34 +- .../configs/feature_toggles_config.json | 4 - .../di/WalletConnectDataModule.kt | 3 - .../sessions/DefaultWcSessionsManager.kt | 43 -- .../walletconnect/model/legacy/Account.kt | 16 - .../walletconnect/model/legacy/Session.kt | 13 - .../legacy/WalletConnectSessionsRepository.kt | 9 - .../components/WalletConnectFeatureToggles.kt | 5 - .../connections/routing/WcRoutingModel.kt | 8 +- .../DefaultWalletConnectDeepLinkHandler.kt | 18 +- gradle/dependencies.toml | 2 - 74 files changed, 52 insertions(+), 4627 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/di/WalletConnectFeatureModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionsDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt delete mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt delete mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt delete mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt delete mode 100644 features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3458dfc6a4..1188d0e283 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -347,7 +347,6 @@ dependencies { implementation(deps.coil.gif) implementation(deps.coil.svg) implementation(deps.amplitude) - implementation(deps.kotsonGson) implementation(deps.spongecastle.core) implementation(deps.lottie) implementation(deps.compose.accompanist.appCompatTheme) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ce53e6dc05..2f4c389818 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -35,6 +35,7 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -50,7 +51,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository @EntryPoint @InstallIn(SingletonComponent::class) @@ -71,8 +71,6 @@ interface ApplicationEntryPoint { fun getCardScanningFeatureToggles(): CardScanningFeatureToggles - fun getWalletConnect2Repository(): WalletConnect2Repository - fun getScanCardProcessor(): ScanCardProcessor fun getAppCurrencyRepository(): AppCurrencyRepository @@ -151,4 +149,6 @@ interface ApplicationEntryPoint { fun getTangemHotSdk(): TangemHotSdk fun getHotWalletFeatureToggles(): HotWalletFeatureToggles + + fun getWcInitializeUseCase(): WcInitializeUseCase } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 659aaec327..7217bbef8d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode 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.models.wallet.isLocked import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase @@ -49,12 +50,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder @@ -64,11 +63,9 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler -import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent @@ -116,9 +113,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var scanCardUseCase: ScanCardUseCase - @Inject - lateinit var walletConnectInteractor: WalletConnectInteractor - @Inject lateinit var settingsRepository: SettingsRepository @@ -171,9 +165,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var deeplinkFactory: DeepLinkFactory - @Inject - internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles - @Inject internal lateinit var urlOpener: UrlOpener @@ -183,9 +174,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var intentProcessor: IntentProcessor - @Inject - internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler - @Inject internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler @@ -201,7 +189,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles - internal val viewModel: MainViewModel by viewModels() + private val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -293,7 +281,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { store.dispatch( DaggerGraphAction.SetActivityDependencies( scanCardUseCase = scanCardUseCase, - walletConnectInteractor = walletConnectInteractor, cardSdkConfigRepository = cardSdkConfigRepository, ), ) @@ -369,10 +356,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private fun initIntentHandlers() { intentProcessor.addHandler(onPushClickedIntentHandler) - - if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - intentProcessor.addHandler(walletConnectLinkIntentHandler) - } } private fun updateAppTheme(appThemeMode: AppThemeMode) { diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 4c35b0ee8f..1ed30f5829 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -75,10 +75,12 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Store import timber.log.Timber -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository lateinit var store: Store @@ -112,9 +114,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val cardScanningFeatureToggles: CardScanningFeatureToggles get() = entryPoint.getCardScanningFeatureToggles() - private val walletConnect2Repository: WalletConnect2Repository - get() = entryPoint.getWalletConnect2Repository() - private val scanCardProcessor: ScanCardProcessor get() = entryPoint.getScanCardProcessor() @@ -237,6 +236,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val hotWalletFeatureToggles get() = entryPoint.getHotWalletFeatureToggles() + private val wcInitializeUseCase + get() = entryPoint.getWcInitializeUseCase() + // endregion private val appScope = MainScope() @@ -331,7 +333,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat ) appStateHolder.mainStore = store - walletConnect2Repository.init( + + wcInitializeUseCase.init( projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, ) } @@ -344,7 +347,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat daggerGraphState = DaggerGraphState( networkConnectionManager = networkConnectionManager, cardScanningFeatureToggles = cardScanningFeatureToggles, - walletConnectRepository = walletConnect2Repository, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 814384c569..c863320c2a 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -5,9 +5,10 @@ import android.content.Context import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState -import com.tangem.tap.common.ui.* -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog -import com.tangem.tap.features.details.ui.walletconnect.dialogs.* +import com.tangem.tap.common.ui.ScanFailsDialog +import com.tangem.tap.common.ui.SimpleAlertDialog +import com.tangem.tap.common.ui.SimpleCancelableAlertDialog +import com.tangem.tap.common.ui.SimpleOkDialog import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog @@ -59,88 +60,6 @@ class DialogManager : StoreSubscriber { context = context, ) is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) - is WalletConnectDialog.UnsupportedCard -> - SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - messageRes = R.string.wallet_connect_scanner_error_not_valid_card, - context = context, - ) - is WalletConnectDialog.AddNetwork -> { - val message = context.getString( - R.string.wallet_connect_error_missing_blockchains, - ) + state.dialog.networks.joinToString() - SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - message = message, - context = context, - ) - } - is WalletConnectDialog.OpeningSessionRejected -> { - SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - messageRes = R.string.wallet_connect_same_wcuri, - context = context, - ) - } - is WalletConnectDialog.SessionTimeout -> { - SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - messageRes = R.string.wallet_connect_error_timeout, - context = context, - ) - } - is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context) - is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context) - is WalletConnectDialog.BnbTransactionDialog -> - BnbTransactionDialog.create( - preparedData = state.dialog.data, - context = context, - ) - is WalletConnectDialog.UnsupportedNetwork -> { - val warning = if (state.dialog.networks.isNullOrEmpty()) { - context.getString(R.string.wallet_connect_scanner_error_unsupported_network) - } else { - context.getString(R.string.wallet_connect_error_unsupported_blockchains) + - state.dialog.networks.joinToString() - } - SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - message = warning, - context = context, - ) - } - is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - messageRes = R.string.wallet_connect_error_unsupported_dapp, - context = context, - ) - is WalletConnectDialog.SessionProposalDialog -> { - SessionProposalDialog.create( - sessionProposal = state.dialog.sessionProposal, - networks = state.dialog.networks, - context = context, - onApprove = state.dialog.onApprove, - onReject = state.dialog.onReject, - ) - } - is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create( - preparedData = state.dialog.data, - context = context, - ) - is WalletConnectDialog.SignTransactionsDialog -> SignTransactionsDialog.create( - preparedData = state.dialog.data, - context = context, - ) - is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create( - titleRes = R.string.wallet_connect_title, - message = state.dialog.error.message, - context = context, - ) - is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create( - titleRes = R.string.common_error, - messageRes = R.string.unsupported_wc_version, - context = context, - ) is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create( context = context, scanResponse = state.dialog.scanResponse, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt deleted file mode 100644 index 24566a94f8..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType - -/** -[REDACTED_AUTHOR] - */ -internal sealed class WalletConnect( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Wallet Connect", event, params) { - - class ScreenOpened : WalletConnect(event = "WC Screen Opened") - class NewSessionInitiated(source: SourceType) : WalletConnect( - event = "Session Initiated", - params = mapOf( - AnalyticsParam.SOURCE to when (source) { - SourceType.QR -> "QR" - SourceType.DEEPLINK -> "DeepLink" - SourceType.CLIPBOARD -> "Clipboard" - SourceType.ETC -> "etc" - }, - ), - ) - - data object SessionFailed : WalletConnect( - event = "Session Failed", - ) - - class DAppConnectionRequested( - blockchainNames: List, - ) : WalletConnect( - event = "dApp Connection Requested", - params = mapOf( - AnalyticsParam.NETWORKS to blockchainNames.joinToString(","), - ), - ) - - class DAppConnected(dAppName: String, dAppUrl: String, blockchainNames: List) : WalletConnect( - event = "dApp Connected", - params = mapOf( - AnalyticsParam.DAPP_NAME to dAppName, - AnalyticsParam.DAPP_URL to dAppUrl, - AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","), - ), - ) - - class DAppConnectionFailed(dAppName: String, dAppUrl: String, blockchainNames: List) : WalletConnect( - event = "dApp Connection Failed", - params = mapOf( - AnalyticsParam.DAPP_NAME to dAppName, - AnalyticsParam.DAPP_URL to dAppUrl, - AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","), - ), - ) - - class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect( - event = "dApp Disconnected", - params = mapOf( - AnalyticsParam.DAPP_NAME to dAppName, - AnalyticsParam.DAPP_URL to dAppUrl, - ), - ) - - class SignatureRequestHandled( - params: RequestHandledParams, - ) : WalletConnect( - event = "Signature Request Handled", - params = params.toParamsMap(), - ) - - class SignatureRequestReceived( - params: RequestHandledParams, - ) : WalletConnect( - event = "Signature Request Received", - params = params.toParamsMap(), - ) - - class SignatureRequestFailed( - params: RequestHandledParams, - ) : WalletConnect( - event = "Signature Request Failed", - params = params.toParamsMap(), - ) - - data class RequestHandledParams( - val dAppName: String, - val dAppUrl: String, - val methodName: String, - val blockchain: String, - val errorCode: String? = null, - val errorDescription: String? = null, - ) { - fun toParamsMap(): Map { - val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param - val code = errorCode ?: SUCCESS_CODE - return buildMap { - put(AnalyticsParam.DAPP_NAME, dAppName) - put(AnalyticsParam.DAPP_URL, dAppUrl) - put(AnalyticsParam.METHOD_NAME, methodName) - put(AnalyticsParam.BLOCKCHAIN, blockchain) - put(AnalyticsParam.VALIDATION, validation) - put(AnalyticsParam.ERROR_CODE, code) - if (errorDescription != null) { - put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription) - } - } - } - } - - enum class Validation(val param: String) { - SUCCESS("Success"), - FAIL("Fail"), - } - - private companion object { - const val SUCCESS_CODE = "0" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 8bd03da756..6099bad60a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -2,7 +2,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState { return AppState( globalState = globalReducer(action, state), detailsState = DetailsReducer.reduce(action, state), - walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), daggerGraphState = DaggerGraphReducer.reduce(action, state), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 427a103a98..7c3065b0f4 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -5,8 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware @@ -19,7 +17,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), val detailsState: DetailsState = DetailsState(), - val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { @@ -30,7 +27,6 @@ data class AppState( logMiddleware, GlobalMiddleware.handler, DetailsMiddleware().detailsMiddleware, - WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, WelcomeMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, diff --git a/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt index cd7becb2e0..b84f990fa6 100644 --- a/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt +++ b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt @@ -4,7 +4,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler -import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,10 +18,6 @@ internal object IntentHandlingModule { @Singleton fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler() - @Provides - @Singleton - fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler() - @Provides @Singleton fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler = diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt deleted file mode 100644 index 0e6ccce36b..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.github.salomonbrys.kotson.registerTypeAdapter -import com.google.gson.GsonBuilder -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.calculateSha256 -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer -import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData -import com.tangem.tap.features.details.redux.walletconnect.TradeData -import com.tangem.utils.extensions.stripZeroPlainString -import timber.log.Timber - -internal object BnbHelper { - - fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer { - val input = order.msgs.first().inputs.first() - val output = order.msgs.first().inputs.first() - - val currency = - input.coins.firstOrNull()?.denom ?: Blockchain.Binance - val amount = input.coins - .mapNotNull { if (it.denom == currency) it.amount else null } - .sum() - .toBigDecimal() - .movePointLeft(Blockchain.Binance.decimals()) - .stripZeroPlainString() - - val gson = GsonBuilder() - .registerTypeAdapter(tradeOrderSerializer) - .serializeNulls() - .create() - - return BinanceMessageData.Transfer( - outputAddress = output.address, - amount = amount, - address = input.address, - data = gson.toJson(order).toByteArray().calculateSha256(), - ) - } - - fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade { - val address = order.msgs.first().sender - - val tradeData = order.msgs.map { - val price = it.price.toBigDecimal() - .movePointLeft(Blockchain.Binance.decimals()) - .stripTrailingZeros() - val quantity = it.quantity.toBigDecimal() - .movePointLeft(Blockchain.Binance.decimals()) - .stripTrailingZeros() - - val amount = price * quantity - val symbol = it.symbol.substringBefore("-") - - TradeData( - price = "$price ${Blockchain.Binance.currency}", - quantity = "$quantity $symbol", - amount = "$amount ${Blockchain.Binance.currency}", - symbol = symbol, - ) - } - val gson = GsonBuilder() - .registerTypeAdapter(tradeOrderSerializer) - .serializeNulls() - .create() - val serialized = gson.toJson(order) - Timber.d(serialized) - - return BinanceMessageData.Trade( - tradeData = tradeData, - address = address, - data = serialized.toByteArray().calculateSha256(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt deleted file mode 100644 index 80d7b046e9..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.github.salomonbrys.kotson.fromJson -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonParser - -object EthSignHelper { - private val gson: Gson by lazy { - GsonBuilder() - .setPrettyPrinting() - .serializeNulls() - .create() - } - - fun tryToParseEthTypedMessageString(message: String): String? { - return try { - val messageString = message - .replace("\\", "") - .removePrefix("\"") - .removeSuffix("\"") - val messageJson = JsonParser().parse(messageString) - val filteredMap = gson.fromJson>(messageJson) - .filterKeys { it == "domain" || it == "message" } - - gson.toJson(filteredMap) - } catch (exception: Exception) { - null - } - } -} \ No newline at end of file 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 deleted file mode 100644 index e6e92ccda4..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ /dev/null @@ -1,529 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.tangem.Message -import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader -import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils -import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.* -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.CompletionResult -import com.tangem.common.extensions.hexToBytes -import com.tangem.common.extensions.toDecompressedPublicKey -import com.tangem.common.extensions.toHexString -import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.operations.sign.SignHashCommand -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.safeUpdate -import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.domain.walletconnect2.domain.TransactionType -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage -import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder -import com.tangem.tap.features.demo.isDemoCard -import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData -import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType -import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData -import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData -import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import org.json.JSONArray -import org.json.JSONObject -import timber.log.Timber -import java.math.BigDecimal - -@Suppress("LargeClass") -class WalletConnectSdkHelper { - - private val userWalletsListManager by lazy { - 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 - val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { "Blockchain not found" } - val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) { - "WalletManager not found" - } - - walletManager.safeUpdate(isDemoCard()) - val wallet = walletManager.wallet - val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) { - "Coin balance not found" - } - - val decimals = wallet.blockchain.decimals() - - val value = (transaction.value ?: "0") - .hexToBigDecimal() - .movePointLeft(decimals) - - requireNotNull(value) { - "Transaction amount is null" - } - - // TODO move fee calculation to SDK getFee() [REDACTED_JIRA] - val gasLimit = getGasLimitFromTx(value, walletManager, transaction, blockchain) - val gasPrice = getGasPrice(walletManager, transaction) - - val feeDecimal = (gasLimit * gasPrice).movePointLeft(decimals) - val total = value + feeDecimal - val feeAmount = Amount(feeDecimal, wallet.blockchain) - - val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } - - val fee = if (blockchain.isEvm()) { - // TODO [REDACTED_JIRA] - // workaround for Mantle, remove after [REDACTED_JIRA] - val patchedAmount = if (blockchain == Blockchain.Mantle) { - feeAmount.copy(value = feeAmount.value?.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)) - } else { - feeAmount - } - Fee.Ethereum.Legacy(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) - } else { - Fee.Common(feeAmount) - } - val transactionData = TransactionData.Uncompiled( - amount = Amount(value, wallet.blockchain), - fee = fee, - sourceAddress = transaction.from, - destinationAddress = destinationAddress, - extras = EthereumTransactionExtras( - callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()), - gasLimit = gasLimit.toBigInteger(), - nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(), - ), - ) - - val dialogData = TransactionRequestDialogData( - dAppName = data.metaName, - dAppUrl = data.metaUrl, - amount = value.toFormattedString(decimals), - feeAmount = feeDecimal.toFormattedString(decimals), - totalAmount = total.toFormattedString(decimals), - balance = balance.toFormattedString(decimals), - isEnoughFundsToSend = balance - total >= BigDecimal.ZERO, - topic = data.topic, - id = data.id, - type = data.type, - ) - - return WcTransactionData( - type = data.type, - transaction = transactionData, - topic = data.topic, - id = data.id, - walletManager = walletManager, - dialogData = dialogData, - ) - } - - 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 = getSelectedWallet() ?: return null - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - return walletManagerFacade.getOrCreateWalletManager( - userWalletId = userWallet.walletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - } - - suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? { - return when (data.type) { - WcEthTransactionType.EthSendTransaction -> sendTransaction(data, cardId) - WcEthTransactionType.EthSignTransaction -> signTransaction(data, cardId) - } - } - - private suspend fun getGasPrice(walletManager: WalletManager, transaction: WcEthereumTransaction): BigDecimal { - val txGasPrice = transaction.gasPrice?.hexToBigDecimal() - if (txGasPrice != null) { - return txGasPrice - } - return when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) { - is Result.Success -> result.data.toBigDecimal() - is Result.Failure -> { - (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") } - - error("Unable to get gas price: ${result.error}") - } - null -> error("Gas price is null") - } - } - - private suspend fun getGasLimitFromTx( - value: BigDecimal, - walletManager: WalletManager, - transaction: WcEthereumTransaction, - blockchain: Blockchain, - ): BigDecimal { - return transaction.gas?.hexToBigDecimal() - ?: transaction.gasLimit?.hexToBigDecimal() - ?: getGasLimitFromBlockchain( - value = value, - walletManager = walletManager, - transaction = transaction, - ).increaseForMantleIfNeeded(blockchain) - } - - private suspend fun getGasLimitFromBlockchain( - value: BigDecimal, - walletManager: WalletManager, - transaction: WcEthereumTransaction, - ): BigDecimal { - val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit( - amount = Amount(value, walletManager.wallet.blockchain), - destination = transaction.to ?: "", - callData = CompiledSmartContractCallData(transaction.data.hexToBytes()), - ) - return when (gasLimitResult) { - is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2")) - is Result.Failure -> { - (gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") } - DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided - } - else -> DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided - } - } - - // TODO Workaround for Mantle. Remove after [REDACTED_JIRA] - private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal { - return if (blockchain == Blockchain.Mantle) { - this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER) - } else { - this - } - } - - private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? { - val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk - - val result = (data.walletManager as TransactionSender).send( - transactionData = data.transaction, - signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner( - cardId = cardId, - sdk = sdk, - twinKey = null, // use null here because twin doesn't support WC - ), - ) - return when (result) { - is Result.Success -> { - val hash = result.data.hash - if (hash.startsWith(HEX_PREFIX)) { - hash - } else { - HEX_PREFIX + hash - } - } - is Result.Failure -> { - Timber.e(result.error as BlockchainSdkError) - null - } - } - } - - private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? { - val dataToSign = EthereumUtils.buildTransactionToSign( - transactionData = data.transaction, - blockchain = data.walletManager.wallet.blockchain, - ) - - val command = SignHashCommand( - hash = dataToSign.hash, - walletPublicKey = data.walletManager.wallet.publicKey.seedKey, - derivationPath = data.walletManager.wallet.publicKey.derivationPath, - ) - return when ( - val result = tangemSdkManager.runTaskAsync( - runnable = command, - initialMessage = Message(), - cardId = cardId, - preflightReadFilter = null, - ) - ) { - is CompletionResult.Success -> { - val hash = EthereumUtils.prepareTransactionToSend( - signature = result.data.signature, - transactionToSign = dataToSign, - walletPublicKey = data.walletManager.wallet.publicKey, - blockchain = data.walletManager.wallet.blockchain, - ).toHexString() - if (hash.startsWith(HEX_PREFIX)) { - hash - } else { - HEX_PREFIX + hash - } - } - is CompletionResult.Failure -> { - Timber.e(result.error.customMessage) - null - } - } - } - - fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade { - return BnbHelper.createMessageData(data) - } - - fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer { - return BnbHelper.createMessageData(data) - } - - suspend fun signBnbTransaction( - data: ByteArray, - networkId: String, - derivationPath: String?, - cardId: String?, - ): String? { - val blockchain = Blockchain.fromNetworkId(networkId) ?: return null - val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null - - val command = SignHashCommand( - hash = data, - walletPublicKey = wallet.publicKey.seedKey, - derivationPath = wallet.publicKey.derivationPath, - ) - return when ( - val result = tangemSdkManager.runTaskAsync( - runnable = command, - initialMessage = Message(), - cardId = cardId, - preflightReadFilter = null, - ) - ) { - is CompletionResult.Success -> { - val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey() - getBnbResultString( - key.toHexString(), - result.data.signature.toHexString(), - ) - } - is CompletionResult.Failure -> { - Timber.e(result.error.customMessage) - null - } - } - } - - fun prepareDataForPersonalSign( - message: WcSignMessage, - topic: String, - metaName: String, - id: Long, - ): WcPersonalSignData { - val messageData = when (message.type) { - WcSignMessage.WCSignType.MESSAGE, - WcSignMessage.WCSignType.PERSONAL_MESSAGE, - -> createMessageData(message) - WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data) - WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58() - } - - val messageString = message.data.hexToAscii() - ?: EthSignHelper.tryToParseEthTypedMessageString(message.data) - ?: message.data - - val dialogData = PersonalSignDialogData( - dAppName = metaName, - message = messageString, - topic = topic, - id = id, - ) - return WcPersonalSignData( - hash = requireNotNull(messageData) { "Message data must not be null" }, - topic = topic, - id = id, - dialogData = dialogData, - type = message.type, - ) - } - - private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data) - - private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this) - - suspend fun signPersonalMessage( - hashToSign: ByteArray, - networkId: String, - type: WcSignMessage.WCSignType, - derivationPath: String?, - cardId: String?, - ): String? { - val blockchain = Blockchain.fromNetworkId(networkId) ?: return null - val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null - - val command = SignHashCommand( - hash = hashToSign, - walletPublicKey = wallet.publicKey.seedKey, - derivationPath = wallet.publicKey.derivationPath, - ) - return when ( - val result = tangemSdkManager.runTaskAsync( - runnable = command, - cardId = cardId, - preflightReadFilter = null, - ) - ) { - is CompletionResult.Success -> { - val signedHash = result.data.signature - - return when (type) { - WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash) - else -> UnmarshalHelper.unmarshalSignatureExtended( - signature = signedHash, - hash = hashToSign, - publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().formatHex() - .lowercase() // use lowercase because some dapps cant handle UPPERCASE - } - } - is CompletionResult.Failure -> { - Timber.e(result.error.customMessage) - null - } - } - } - - /** - * Returns result of signing prepared to send in WC request - */ - suspend fun signTransaction( - hashToSign: ByteArray, - networkId: String, - type: TransactionType, - derivationPath: String?, - cardId: String?, - ): String? { - val blockchain = Blockchain.fromNetworkId(networkId) ?: return null - val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null - - val command = SignHashCommand( - hash = hashToSign, - walletPublicKey = wallet.publicKey.seedKey, - derivationPath = wallet.publicKey.derivationPath, - ) - return when ( - val result = tangemSdkManager.runTaskAsync( - runnable = command, - cardId = cardId, - preflightReadFilter = null, - ) - ) { - is CompletionResult.Success -> { - val signedHash = result.data.signature - - when (type) { - TransactionType.SOLANA_TX -> { - getSolanaResultString(signedHash) - } - } - } - is CompletionResult.Failure -> { - Timber.e(result.error.customMessage) - null - } - } - } - - /** - * Returns result of signing prepared to send in WC request - */ - suspend fun signTransactions( - hashesToSign: List, - networkId: String, - type: TransactionType, - derivationPath: String?, - cardId: String?, - ): String? { - val blockchain = Blockchain.fromNetworkId(networkId) ?: return null - val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null - val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk - - val signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner( - cardId = cardId, - sdk = sdk, - twinKey = null, - ) - - return when (val signingResult = signer.sign(hashesToSign, wallet.publicKey)) { - is CompletionResult.Failure -> { - Timber.e(signingResult.error.customMessage) - null - } - is CompletionResult.Success -> { - when (type) { - TransactionType.SOLANA_TX -> { - val result = signingResult.data.mapIndexed { index, bytes -> - byteArrayOf(1) + bytes + hashesToSign[index] - } - getSolanaResultTxHashesString(result) - } - } - } - else -> { - null - } - } - } - - private suspend fun getSelectedWallet(): UserWallet? { - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.selectedUserWalletSync() - } else { - userWalletsListManager.selectedUserWalletSync - } - } - - private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }" - - /** - * Build json object - * { - * "transactions": [ - * "signed_tx_hash" - * ] - * } - */ - private fun getSolanaResultTxHashesString(signedHashes: List): String { - val result = JSONObject() - val transactions = JSONArray() - signedHashes.forEach { - transactions.put(it.encodeBase64()) - } - result.put("transactions", transactions) - return result.toString() - } - - private fun getBnbResultString(publicKey: String, signature: String) = - "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}" - - private companion object { - const val HEX_PREFIX = "0x" - const val DEFAULT_MAX_GASLIMIT = 350000 - // TODO remove after [REDACTED_JIRA] - private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt deleted file mode 100644 index b0e2c44579..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.app - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper -import com.tangem.domain.walletconnect.model.legacy.Account - -internal class TangemWcBlockchainHelper : WcBlockchainHelper { - - private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet) - - override fun chainIdToNetworkIdOrNull(chainId: String): String? { - val parsedId = chainId.parseId() ?: return null - val blockchain = parsedId.chainIdToBlockchain() - - return blockchain?.toNetworkId() - } - - override fun chainIdsToBlockchains(chainIds: List): List { - return chainIds.mapNotNull { - it.parseId()?.chainIdToBlockchain() - }.distinct() - } - - override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? { - val parsedId = chainId.parseId() ?: return null - val blockchain = parsedId.chainIdToBlockchain() - - return blockchain?.fullName - ?: if (parsedId.first == EVM_NAMESPACE) { - chainId - } else { - parsedId.first.replaceFirstChar(Char::titlecase) - } - } - - override fun networkIdToChainIdOrNull(networkId: String): List { - val blockchain = Blockchain.fromNetworkId(networkId) - val namespace = blockchain?.getCaip2Namespace() ?: return emptyList() - return blockchain.getCaip2ChainIds().map { - "$namespace$CHAIN_SEPARATOR$it" - } - } - - override fun getNamespaceFromFullChainIdOrNull(chainId: String): String? { - val parsed = chainId.split(CHAIN_SEPARATOR) - return parsed.firstOrNull() - } - - override fun chainIdToFullNameOrNull(chainId: String): String? { - val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null - return Blockchain.fromNetworkId(networkId)?.fullName - } - - override fun chainIdsToAccounts( - walletAddress: String, - chainIds: List, - derivationPath: String?, - ): List { - return chainIds.map { chainId -> - Account(chainId, walletAddress, derivationPath) - } - } - - private fun Blockchain.getCaip2ChainIds(): List { - if (this.isEvm()) return listOfNotNull(this.getChainId()?.toString()) - - return when (this) { - /* - * The WC sample application and documentation use a commented out chain ID. However, in real dApps - * uncommented is used. - * Docs: https://docs.walletconnect.com/advanced/multichain/chain-list - * - * */ - Blockchain.Solana -> listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ") - Blockchain.SolanaTestnet -> listOf("z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z") - Blockchain.Polkadot -> listOf("91b171bb158e2d3848fa23a9f1c25182") - Blockchain.Tron -> listOf("0x2b6653dc") - else -> emptyList() - } - } - - private fun Blockchain.getCaip2Namespace(): String? { - return when { - this.isEvm() -> EVM_NAMESPACE - supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR) - else -> null - } - } - - private fun String.parseId(): Pair? { - val parsed = this.split(CHAIN_SEPARATOR) - if (parsed.size != 2) return null - - return parsed[0] to parsed[1] - } - - private fun Pair.chainIdToBlockchain(): Blockchain? { - return when (first) { - EVM_NAMESPACE -> { - second.toIntOrNull() - ?.let(Blockchain::fromChainId) - } - SOLANA_NAMESPACE -> Blockchain.Solana - else -> { - Blockchain.fromNetworkId(networkId = first) - .takeIf(supportedNonEvmBlockchains::contains) - } - } - } - - private companion object { - const val EVM_NAMESPACE = "eip155" - const val SOLANA_NAMESPACE = "solana" - const val CHAIN_SEPARATOR = ":" - const val TESTNET_SEPARATOR = "/" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt deleted file mode 100644 index d84263034d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.app - -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectEventsHandler -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog -import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen -import com.tangem.tap.store -import timber.log.Timber - -internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { - override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.SessionProposalDialog( - sessionProposal = proposal, - networks = networksFormatted, - onApprove = { store.dispatchOnMain(WalletConnectAction.ApproveProposal(proposal)) }, - onReject = { store.dispatchOnMain(WalletConnectAction.RejectProposal) }, - ), - ), - ) - } - - override fun onSessionEstablished() { - store.dispatchOnMain(WalletConnectAction.SessionEstablished) - } - - override fun onSessionRejected(error: WalletConnectError) { - store.dispatchOnMain(WalletConnectAction.SessionRejected(error)) - } - - override fun onListOfSessionsUpdated(sessions: List) { - Timber.d("WC2: List of sessions updated. Sessions: $sessions") - store.dispatchOnMain(WalletConnectAction.SessionListUpdated(sessions)) - } - - override fun onSessionRequest(request: WcPreparedRequest) { - store.dispatchOnMain(WalletConnectAction.ShowSessionRequest(request)) - } - - override fun onUnsupportedRequest() { - store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest) - } - - override fun onPairConnectError(error: Throwable) { - store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt deleted file mode 100644 index 7d1b854d10..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ /dev/null @@ -1,686 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.data - -import android.app.Application -import arrow.core.flatten -import com.reown.android.Core -import com.reown.android.CoreClient -import com.reown.android.relay.ConnectionType -import com.reown.walletkit.client.Wallet -import com.reown.walletkit.client.WalletKit -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.data.walletconnect.pair.UnsupportedDApps -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletconnect.WcPairService -import com.tangem.domain.walletconnect.model.WcPairRequest -import com.tangem.domain.walletconnect.model.legacy.Account -import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles -import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository -import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods -import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer -import com.tangem.tap.domain.walletconnect2.domain.WcRequest -import com.tangem.tap.domain.walletconnect2.domain.models.* -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.emptyFlow -import timber.log.Timber - -internal class DefaultLegacyWalletConnectRepositoryFacade constructor( - private val stub: LegacyWalletConnectRepositoryStub, - private val legacy: DefaultLegacyWalletConnectRepository, - private val walletConnectFeatureToggles: WalletConnectFeatureToggles, - private val wcInitializeUseCase: WcInitializeUseCase, - private val wcPairService: WcPairService, -) : LegacyWalletConnectRepository { - private val isNewWc by lazy { - walletConnectFeatureToggles.isRedesignedWalletConnectEnabled - } - override val events: Flow by lazy { - if (isNewWc) stub.events else legacy.events - } - override val activeSessions: Flow> by lazy { - if (isNewWc) stub.activeSessions else legacy.activeSessions - } - override val currentSessions: List by lazy { - if (isNewWc) stub.currentSessions else legacy.currentSessions - } - - override fun init(projectId: String) { - if (isNewWc) { - wcInitializeUseCase.init(projectId) - } else { - legacy.init(projectId) - } - } - - override fun setUserNamespaces(userNamespaces: Map>) { - if (isNewWc) stub.setUserNamespaces(userNamespaces) else legacy.setUserNamespaces(userNamespaces) - } - - override fun updateSessions() { - if (isNewWc) stub.updateSessions() else legacy.updateSessions() - } - - override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { - val src = when (source) { - SourceType.QR -> WcPairRequest.Source.QR - SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK - SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD - SourceType.ETC -> WcPairRequest.Source.ETC - } - if (isNewWc) { - wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId)) - } else { - legacy.pair(userWalletId = userWalletId, uri = uri, source = source) - } - } - - override fun disconnect(topic: String) { - if (isNewWc) stub.disconnect(topic) else legacy.disconnect(topic) - } - - override fun approve(userNamespaces: Map>, blockchainNames: List) { - if (isNewWc) stub.approve(userNamespaces, blockchainNames) else legacy.approve(userNamespaces, blockchainNames) - } - - override fun reject() { - if (isNewWc) stub.reject() else legacy.reject() - } - - override fun sendRequest(requestData: RequestData, result: String) { - if (isNewWc) stub.sendRequest(requestData, result) else legacy.sendRequest(requestData, result) - } - - override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { - if (isNewWc) stub.rejectRequest(requestData, error) else legacy.rejectRequest(requestData, error) - } - - override fun cancelRequest(topic: String, id: Long, message: String) { - if (isNewWc) stub.cancelRequest(topic, id, message) else legacy.cancelRequest(topic, id, message) - } -} - -internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository { - override val events: Flow = emptyFlow() - override val activeSessions: Flow> = emptyFlow() - override val currentSessions: List = listOf() - - override fun init(projectId: String) = Unit - - override fun setUserNamespaces(userNamespaces: Map>) = Unit - - override fun updateSessions() = Unit - - override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit - - override fun disconnect(topic: String) = Unit - - override fun approve(userNamespaces: Map>, blockchainNames: List) = Unit - - override fun reject() = Unit - - override fun sendRequest(requestData: RequestData, result: String) = Unit - - override fun rejectRequest(requestData: RequestData, error: WalletConnectError) = Unit - - override fun cancelRequest(topic: String, id: Long, message: String) = Unit -} - -@Suppress("LargeClass") -internal class DefaultLegacyWalletConnectRepository( - private val application: Application, - private val wcRequestDeserializer: WcJrpcRequestsDeserializer, - private val analyticsHandler: AnalyticsEventHandler, -) : LegacyWalletConnectRepository { - - private var sessionProposal: Wallet.Model.SessionProposal? = null - private var userNamespaces: Map>? = null - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val _events: MutableSharedFlow = MutableSharedFlow() - override val events: Flow = _events - - private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() - override val activeSessions: Flow> = _activeSessions - private val blockchainHelper by lazy { TangemWcBlockchainHelper() } - - override var currentSessions: List = emptyList() - private set - - /** - * @param projectId Project ID at https://cloud.walletconnect.com/ - */ - override fun init(projectId: String) { - val relayUrl = "relay.walletconnect.com" - val serverUrl = "wss://$relayUrl?projectId=$projectId" - val connectionType = ConnectionType.AUTOMATIC - - val appMetaData = Core.Model.AppMetaData( - name = "Tangem", - description = "Tangem Wallet", - url = "tangem.com", - icons = listOf( - "https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png", - ), - redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI - ) - - CoreClient.initialize( - relayServerUrl = serverUrl, - connectionType = connectionType, - application = application, - metaData = appMetaData, - ) { error -> - Timber.e("Error while initializing client: $error") - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ExternalApprovalError(error.throwable.message), - ), - ) - } - } - - WalletKit.initialize( - Wallet.Params.Init(core = CoreClient), - onSuccess = { - val walletDelegate = defineWalletDelegate() - WalletKit.setWalletDelegate(walletDelegate) - }, - onError = { error -> - Timber.e("Error while initializing Web3Wallet: $error") - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ExternalApprovalError(error.throwable.message), - ), - ) - } - }, - ) - } - - private fun defineWalletDelegate(): WalletKit.WalletDelegate { - return object : WalletKit.WalletDelegate { - - @Suppress("LongMethod") - override fun onSessionProposal( - sessionProposal: Wallet.Model.SessionProposal, - verifyContext: Wallet.Model.VerifyContext, - ) { - // Triggered when wallet receives the session proposal sent by a Dapp - Timber.i("sessionProposal: $sessionProposal") - this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal - - if (sessionProposal.name in UnsupportedDApps.list) { - Timber.i("Unsupported DApp") - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.UnsupportedDApp, - ), - ) - } - return - } - - val missingNetworks = findMissingNetworks( - namespaces = sessionProposal.requiredNamespaces, - userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), - ) - - if (missingNetworks.isNotEmpty()) { - Timber.i("Not added blockchains: $missingNetworks") - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()), - ), - ) - } - return - } - - val optionalMissingNetwork = findMissingNetworks( - namespaces = sessionProposal.optionalNamespaces, - userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), - ) - - val optionalWithoutMissingNetworks = removeMissingNetworks( - namespaces = sessionProposal.optionalNamespaces, - userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), - ) - - // for cases when optionalNamespaces is not empty but we doesn't support none of them - if (optionalMissingNetwork.isNotEmpty() && - optionalWithoutMissingNetworks.isEmpty() && - sessionProposal.requiredNamespaces.isEmpty() // if requiredNamespaces is not empty we can connect - ) { - Timber.i("Not added optional blockchains: $optionalMissingNetwork") - - val unsupportedNetworks = sessionProposal.optionalNamespaces.values - .flatMap { it.chains ?: emptyList() } - .filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null } - - scope.launch { - val error = if (unsupportedNetworks.isNotEmpty()) { - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks), - ) - } else { - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ApprovalErrorMissingNetworks(optionalMissingNetwork.toList()), - ) - } - _events.emit(error) - } - return - } - - val requiredChainIds = sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() } - val optionalChainIds = optionalWithoutMissingNetworks.toList() - val networks = (requiredChainIds + optionalChainIds) - .mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) } - .distinct() - analyticsHandler.send(WalletConnect.DAppConnectionRequested(networks)) - - scope.launch { - _events.emit( - WalletConnectEvents.SessionProposal( - sessionProposal.name, - sessionProposal.description, - sessionProposal.url, - sessionProposal.icons, - requiredChainIds, - optionalChainIds, - ), - ) - } - } - - override fun onSessionRequest( - sessionRequest: Wallet.Model.SessionRequest, - verifyContext: Wallet.Model.VerifyContext, - ) { - // Triggered when a Dapp sends SessionRequest to sign a transaction or a message - Timber.i("sessionRequest: $sessionRequest") - val request = wcRequestDeserializer.deserialize( - method = sessionRequest.request.method, - params = sessionRequest.request.params, - ) - Timber.i("sessionRequestParsed: $request") - - when (request) { - is WcRequest.AddChain -> { - // we can send approval automatically, because in WC 2.0 the list of chains is approved when - // initial connection is established - sendRequest( - RequestData( - topic = sessionRequest.topic, - requestId = sessionRequest.request.id, - blockchain = sessionRequest.chainId.toString(), - method = WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code, - ), - result = "", - ) - } - else -> { - val event = WalletConnect.SignatureRequestReceived( - WalletConnect.RequestHandledParams( - dAppName = sessionRequest.peerMetaData?.name ?: "", - dAppUrl = sessionRequest.peerMetaData?.url ?: "", - methodName = sessionRequest.request.method, - blockchain = sessionRequest.chainId - ?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "", - ), - ) - analyticsHandler.send(event) - scope.launch { - _events.emit( - WalletConnectEvents.SessionRequest( - request = request, - chainId = sessionRequest.chainId, - topic = sessionRequest.topic, - id = sessionRequest.request.id, - metaUrl = sessionRequest.peerMetaData?.url ?: "", - metaName = sessionRequest.peerMetaData?.name ?: "", - method = sessionRequest.request.method, - ), - ) - } - } - } - } - - override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - // Triggered when the session is deleted by the peer - if (sessionDelete is Wallet.Model.SessionDelete.Success) { - scope.launch { - _events.emit(WalletConnectEvents.SessionDeleted(sessionDelete.topic)) - updateSessionsInternal().join() - } - } - Timber.i("onSessionDelete: $sessionDelete") - } - - override fun onSessionExtend(session: Wallet.Model.Session) { - Timber.i("onSessionExtend: $session") - } - - override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { - // Triggered when wallet receives the session settlement response from Dapp - Timber.i("onSessionSettleResponse: $settleSessionResponse") - if (settleSessionResponse is Wallet.Model.SettledSessionResponse.Result) { - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalSuccess( - topic = settleSessionResponse.session.topic, - accounts = userNamespaces?.flatMap { it.value } ?: emptyList(), - ), - ) - updateSessionsInternal().join() - } - } - } - - override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { - // Triggered when wallet receives the session update response from Dapp - Timber.i("onSessionUpdateResponse: $sessionUpdateResponse") - updateSessionsInternal() - } - - override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { - // Triggered whenever the connection state is changed - Timber.i("onConnectionStateChange: $state") - if (state.isAvailable) updateSessionsInternal() - } - - override fun onError(error: Wallet.Model.Error) { - // Triggered whenever there is an issue inside the SDK - Timber.i("onError: $error") - } - } - } - - override fun setUserNamespaces(userNamespaces: Map>) { - this.userNamespaces = userNamespaces - } - - override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { - analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source)) - WalletKit.pair( - params = Wallet.Params.Pair(uri), - onSuccess = { - Timber.i("Paired successfully: $it") - }, - onError = { - Timber.e("Error while pairing: $it") - analyticsHandler.send(WalletConnect.SessionFailed) - scope.launch { - _events.emit( - WalletConnectEvents.PairConnectError(it.throwable), - ) - } - }, - ) - } - - override fun approve(userNamespaces: Map>, blockchainNames: List) { - val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) - - val userChains = userNamespaces.flatMap { namespace -> - namespace.value.map { it.chainId to "${it.chainId}:${it.walletAddress}" } - }.groupBy { pair -> pair.first } - .mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() } - - val preparedRequiredNamespaces = sessionProposal.requiredNamespaces - .map { requiredNamespace -> - val accountsRequired = requiredNamespace.value.chains - ?.mapNotNull { chain -> userChains[chain] } - ?.flatten() ?: emptyList() - val optionalNamespace = sessionProposal.optionalNamespaces[requiredNamespace.key] - val accountsOptional = optionalNamespace?.chains - ?.mapNotNull { chain -> userChains[chain] } - ?.flatten() ?: emptyList() - - val methods = (requiredNamespace.value.methods + (optionalNamespace?.methods ?: emptyList())) - .distinct() - requiredNamespace.key to Wallet.Model.Namespace.Session( - accounts = (accountsRequired + accountsOptional).distinct(), - methods = methods, - events = requiredNamespace.value.events, - ) - }.toMap() - - val sessionApproval = Wallet.Params.SessionApprove( - proposerPublicKey = sessionProposal.proposerPublicKey, - namespaces = preparedRequiredNamespaces.ifEmpty { - sessionProposal.createPreparedOptionalNamespaces(userChains) - }.filterValues { it.accounts.isNotEmpty() && it.chains?.isNotEmpty() == true }, - ) - - Timber.i("Session approval is prepared for sending: $sessionApproval") - - WalletKit.approveSession( - params = sessionApproval, - onSuccess = { - Timber.i("Approved successfully: $it") - analyticsHandler.send( - WalletConnect.DAppConnected( - dAppName = sessionProposal.name, - dAppUrl = sessionProposal.url, - blockchainNames = blockchainNames, - ), - ) - }, - onError = { - Timber.e("Error while approving: $it") - analyticsHandler.send( - WalletConnect.DAppConnectionFailed( - dAppName = sessionProposal.name, - dAppUrl = sessionProposal.url, - blockchainNames = blockchainNames, - ), - ) - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ExternalApprovalError(it.throwable.message), - ), - ) - } - }, - ) - } - - private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces( - userChains: Map>, - ): Map { - return optionalNamespaces - .map { optionalNamespace -> - val accountsOptional = optionalNamespace.value.chains - ?.mapNotNull { chain -> userChains[chain] } - ?.flatten() ?: emptyList() - - val methods = optionalNamespace.value.methods - optionalNamespace.key to Wallet.Model.Namespace.Session( - accounts = accountsOptional.distinct(), - methods = methods, - chains = userChains.keys - .filter { it.startsWith(optionalNamespace.key) } - .toList(), - events = optionalNamespace.value.events, - ) - } - .toMap() - } - - override fun sendRequest(requestData: RequestData, result: String) { - val session = currentSessions.find { it.topic == requestData.topic } - // Add Ethereum Chain method is processed without user input, skip logging it - if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) { - analyticsHandler.send( - WalletConnect.SignatureRequestHandled( - WalletConnect.RequestHandledParams( - dAppName = session?.name ?: "", - dAppUrl = session?.url ?: "", - methodName = requestData.method, - blockchain = requestData.blockchain, - ), - ), - ) - } - - val params = Wallet.Params.SessionRequestResponse( - sessionTopic = requestData.topic, - jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( - id = requestData.requestId, - result = result, - ), - ) - Timber.i("Session request response: $params") - - WalletKit.respondSessionRequest( - params = params, - onSuccess = { response -> - Timber.i("Session request responded successfully: $response") - }, - onError = { error -> - Timber.e(error.throwable, "Error while responging session request") - - val handledParams = WalletConnect.RequestHandledParams( - dAppName = session?.name ?: "", - dAppUrl = session?.url ?: "", - methodName = requestData.method, - blockchain = requestData.blockchain, - errorCode = WalletConnectError.ValidationError.error, - errorDescription = error.throwable.message, - ) - analyticsHandler.send(WalletConnect.SignatureRequestFailed(handledParams)) - }, - ) - } - - override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { - val session = currentSessions.find { it.topic == requestData.topic } - - analyticsHandler.send( - WalletConnect.SignatureRequestFailed( - WalletConnect.RequestHandledParams( - dAppName = session?.name ?: "", - dAppUrl = session?.url ?: "", - methodName = requestData.method, - blockchain = requestData.blockchain, - errorCode = error.toString(), - ), - ), - ) - - cancelRequest(requestData.topic, requestData.requestId, error.error) - } - - override fun cancelRequest(topic: String, id: Long, message: String) { - WalletKit.respondSessionRequest( - params = Wallet.Params.SessionRequestResponse( - sessionTopic = topic, - jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( - id = id, - code = 0, - message = message, - ), - ), - onSuccess = {}, - onError = {}, - ) - } - - override fun reject() { - WalletKit.rejectSession( - params = Wallet.Params.SessionReject( - proposerPublicKey = sessionProposal?.proposerPublicKey ?: "", - reason = "", - ), - onSuccess = { - Timber.i("Rejected successfully: $it") - }, - onError = { - Timber.e("Error while rejecting: $it") - }, - ) - } - - override fun disconnect(topic: String) { - val session = currentSessions.find { it.topic == topic } - WalletKit.disconnectSession( - params = Wallet.Params.SessionDisconnect(topic), - onSuccess = { - analyticsHandler.send( - WalletConnect.SessionDisconnected( - dAppName = session?.name ?: "", - dAppUrl = session?.url ?: "", - ), - ) - updateSessionsInternal() - Timber.i("Disconnected successfully: $it") - }, - onError = { - Timber.e("Error while disconnecting: $it") - }, - ) - } - - fun send(topic: String, id: Long, data: String) { - WalletKit.respondSessionRequest( - params = Wallet.Params.SessionRequestResponse( - sessionTopic = topic, - jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( - id = id, - result = data, - ), - ), - onError = {}, - onSuccess = {}, - ) - } - - override fun updateSessions() { - updateSessionsInternal() - } - - private fun updateSessionsInternal(): Job = scope.launch { - val availableSessions = WalletKit.getListOfActiveSessions() - .map { - WalletConnectSession( - topic = it.topic, - icon = it.metaData?.icons?.firstOrNull(), - name = it.metaData?.name, - url = it.metaData?.url, - ) - } - Timber.i("Available sessions: $availableSessions") - currentSessions = availableSessions - _activeSessions.emit(availableSessions) - } - - private fun findMissingNetworks( - namespaces: Map, - userNamespaces: Map>, - ): Collection { - val requiredChains = namespaces.values.flatMap { it.chains ?: emptyList() } - val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } - return requiredChains.subtract(userChains.toSet()) - } - - private fun removeMissingNetworks( - namespaces: Map, - userNamespaces: Map>, - ): Collection { - val wcProvidedChains = namespaces.values.flatMap { it.chains ?: emptyList() } - val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } - return wcProvidedChains.intersect(userChains.toSet()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt deleted file mode 100644 index 108492f2de..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.data - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.datasource.files.FileReader -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository -import com.tangem.domain.walletconnect.model.legacy.Session -import timber.log.Timber - -internal class DefaultWalletConnectSessionsRepository( - private val moshi: Moshi, - private val fileReader: FileReader, -) : - WalletConnectSessionsRepository { - - private val sessionsAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, Session::class.java), - ) - - override suspend fun loadSessions(userWallet: String): List { - return try { - val fileContent = fileReader.readFile(getFileNameForUserWallet(userWallet)) - sessionsAdapter.fromJson(fileContent) ?: emptyList() - } catch (exception: Exception) { - Timber.d(exception) - emptyList() - } - } - - override suspend fun saveSession(userWallet: String, session: Session) { - val updatedList = loadSessions(userWallet).plus(session) - writeSessionToFile(sessions = updatedList, userWallet = userWallet) - } - - override suspend fun removeSession(userWallet: String, topic: String) { - val updatedList = loadSessions(userWallet).filterNot { it.topic == topic } - writeSessionToFile(sessions = updatedList, userWallet = userWallet) - } - - private fun writeSessionToFile(sessions: List, userWallet: String) { - val serialized = sessionsAdapter.toJson(sessions) - try { - fileReader.rewriteFile(serialized, getFileNameForUserWallet(userWallet)) - } catch (exception: Exception) { - Timber.e(exception) - } - } - - companion object { - private const val FILE_NAME_PREFIX = "wc_2" - - private fun getFileNameForUserWallet(userWallet: String): String { - return "$FILE_NAME_PREFIX-${userWallet.uppercase()}" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt deleted file mode 100644 index 6c9e9f189d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.di - -import android.app.Application -import com.squareup.moshi.Moshi -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.datasource.di.SdkMoshi -import com.tangem.datasource.files.FileReader -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletconnect.WcPairService -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository -import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles -import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper -import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper -import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl -import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository -import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepositoryFacade -import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect2.data.LegacyWalletConnectRepositoryStub -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer -import com.tangem.tap.domain.walletconnect2.toggles.DefaultWalletConnectFeatureToggles -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object WalletConnectInteractorModule { - - @Provides - @Singleton - fun provideWalletConnectInteractor( - wcRepository: LegacyWalletConnectRepository, - wcSessionsRepository: WalletConnectSessionsRepository, - currenciesRepository: CurrenciesRepository, - walletManagersFacade: WalletManagersFacade, - walletConnectFeatureToggles: WalletConnectFeatureToggles, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - getSelectedWalletUseCase: GetSelectedWalletUseCase, - ): WalletConnectInteractor { - return WalletConnectInteractor( - handler = WalletConnectEventsHandlerImpl(), - walletConnectRepository = wcRepository, - sessionsRepository = wcSessionsRepository, - sdkHelper = WalletConnectSdkHelper(), - blockchainHelper = TangemWcBlockchainHelper(), - currenciesRepository = currenciesRepository, - walletManagersFacade = walletManagersFacade, - getSelectedWalletUseCase = getSelectedWalletUseCase, - dispatchers = coroutineDispatcherProvider, - walletConnectFeatureToggles = walletConnectFeatureToggles, - ) - } -} - -@Module -@InstallIn(SingletonComponent::class) -internal object WalletConnectModule { - - @Provides - @Singleton - fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles { - return DefaultWalletConnectFeatureToggles(featureTogglesManager) - } - - @Provides - @Singleton - fun provideWalletConnectRepository( - application: Application, - wcRequestDeserializer: WcJrpcRequestsDeserializer, - analyticsHandler: AnalyticsEventHandler, - walletConnectFeatureToggles: WalletConnectFeatureToggles, - wcInitializeUseCase: WcInitializeUseCase, - wcPairService: WcPairService, - ): LegacyWalletConnectRepository { - val legacy = DefaultLegacyWalletConnectRepository( - application = application, - wcRequestDeserializer = wcRequestDeserializer, - analyticsHandler = analyticsHandler, - ) - val stub = LegacyWalletConnectRepositoryStub() - return DefaultLegacyWalletConnectRepositoryFacade( - stub, - legacy, - walletConnectFeatureToggles, - wcInitializeUseCase, - wcPairService, - ) - } - - @Provides - @Singleton - fun provideWalletConnectSessionsRepository( - @SdkMoshi moshi: Moshi, - fileReader: FileReader, - ): WalletConnectSessionsRepository { - return DefaultWalletConnectSessionsRepository( - moshi = moshi, - fileReader = fileReader, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt deleted file mode 100644 index 55b4899aa9..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletconnect.model.legacy.Account -import com.tangem.tap.domain.walletconnect2.domain.models.* -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType -import kotlinx.coroutines.flow.Flow - -interface LegacyWalletConnectRepository { - - val events: Flow - - val activeSessions: Flow> - - val currentSessions: List - - fun init(projectId: String) - - fun setUserNamespaces(userNamespaces: Map>) - - fun updateSessions() - - fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) - - fun disconnect(topic: String) - - fun approve(userNamespaces: Map>, blockchainNames: List) - - fun reject() - - fun sendRequest(requestData: RequestData, result: String) - - fun rejectRequest(requestData: RequestData, error: WalletConnectError) - - fun cancelRequest(topic: String, id: Long, message: String = "") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt deleted file mode 100644 index b9414d247e..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen - -interface WalletConnectEventsHandler { - fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) - - fun onSessionEstablished() - - fun onSessionRejected(error: WalletConnectError) - - fun onListOfSessionsUpdated(sessions: List) - - fun onSessionRequest(request: WcPreparedRequest) - - fun onUnsupportedRequest() - - fun onPairConnectError(error: Throwable) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt deleted file mode 100644 index 395ad23885..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ /dev/null @@ -1,457 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import arrow.core.flatten -import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.blockchainsdk.utils.toNetworkId -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.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletconnect.model.legacy.Account -import com.tangem.domain.walletconnect.model.legacy.Session -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper -import com.tangem.tap.domain.walletconnect2.domain.models.* -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.Stack - -@Suppress("LargeClass", "LongParameterList") -class WalletConnectInteractor( - private val handler: WalletConnectEventsHandler, - private val walletConnectRepository: LegacyWalletConnectRepository, - private val sessionsRepository: WalletConnectSessionsRepository, - private val sdkHelper: WalletConnectSdkHelper, - private val dispatchers: CoroutineDispatcherProvider, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val walletConnectFeatureToggles: WalletConnectFeatureToggles, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - val blockchainHelper: WcBlockchainHelper, -) { - private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } - - private var isWalletConnectReadyForDeepLinks = false - - private val wcScope = CoroutineScope( - SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> - Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") - }, - ) - - private val listenerScope = CoroutineScope( - SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> - Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") - }, - ) - - /** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */ - private val deeplinkStack: Stack = Stack() - - private val events = walletConnectRepository.events - private val sessions = walletConnectRepository.activeSessions - - private var userWalletId: String = "" - private var cardId: String? = null - - private var currentRequest: WalletConnectEvents.SessionRequest? = null - private val sessionRequestConverter = WcSessionRequestConverter( - blockchainHelper = blockchainHelper, - sessionsRepository = sessionsRepository, - sdkHelper = sdkHelper, - ) - - init { - getSelectedWalletUseCase().onRight { userWalletFlow -> - userWalletFlow - .conflate() - .distinctUntilChanged() - .onEach(::initWithWallet) - .flowOn(dispatchers.io) - .launchIn(wcScope) - } - } - - private fun initWithWallet(userWallet: UserWallet) { - if (isNewWc) return - if (userWallet.isMultiCurrency) { - Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}") - startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet)) - subscribeOnCurrenciesUpdates(userWallet) - } - } - - private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) { - currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId) - .conflate() - .distinctUntilChanged() - .onEach { currencies -> - setupUserChains(userWallet, currencies) - } - .flowOn(dispatchers.io) - .launchIn(wcScope) - } - - private suspend fun setupUserChains(userWallet: UserWallet, currencies: List) { - val accounts = getAccountsForWc( - userWallet = userWallet, - networks = currencies.map { it.network }.distinct(), - ) - setUserChains(accounts) - handleDeeplinkStack(accounts) - } - - private fun startListeningWc(userWalletId: String, cardId: String?) { - this.userWalletId = userWalletId - this.cardId = cardId - listenerScope.coroutineContext.cancelChildren() - listenerScope.launch { - launch { subscribeToEvents() } - launch { subscribeToSessions() } - walletConnectRepository.updateSessions() - } - } - - private fun setUserChains(accounts: List) { - val userNamespaces: Map> = accounts - .groupBy { account -> - blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) - ?.let { NetworkNamespace(it) } - }.filterNotNull() - walletConnectRepository.setUserNamespaces(userNamespaces) - } - - private fun handleDeeplinkStack(accounts: List) { - runCatching { - if (accounts.isEmpty()) return - isWalletConnectReadyForDeepLinks = true - if (deeplinkStack.empty()) return - val lastDeeplink = deeplinkStack.pop() - val action = WalletConnectAction - .OpenSession( - wcUri = lastDeeplink, - source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, - userWalletId = UserWalletId(userWalletId), - ) - store.dispatchOnMain(action) - }.onFailure { - Timber.e("WC deeplink handling failed. $it") - } - } - - private suspend fun subscribeToEvents() { - events - .onEach { wcEvent -> - Timber.i("WalletConnect: event: $wcEvent") - when (wcEvent) { - is WalletConnectEvents.SessionProposal -> { - val unsupportedNetworks = wcEvent.requiredChainIds - .filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null } - if (unsupportedNetworks.isNotEmpty()) { - val error = WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks) - handler.onSessionRejected(error) - return@onEach - } - val networksFormatted = (wcEvent.requiredChainIds + wcEvent.optionalChainIds) - .mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) } - .distinct() - .toString() - handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted) - } - is WalletConnectEvents.SessionApprovalError -> { - val error = when (wcEvent.error) { - is WalletConnectError.ApprovalErrorMissingNetworks -> { - val missingNetworks = wcEvent.error.missingChains.mapNotNull { - blockchainHelper.chainIdToMissingNetworkNameOrNull(it) - } - - WalletConnectError.ApprovalErrorAddNetwork(missingNetworks) - } - else -> wcEvent.error - } - handler.onSessionRejected(error) - } - is WalletConnectEvents.SessionApprovalSuccess -> { - sessionsRepository.saveSession( - userWallet = userWalletId, - session = Session( - accounts = wcEvent.accounts, - topic = wcEvent.topic, - ), - ) - walletConnectRepository.updateSessions() - handler.onSessionEstablished() - } - is WalletConnectEvents.SessionDeleted -> { - sessionsRepository.removeSession(userWalletId, wcEvent.topic) - } - is WalletConnectEvents.SessionRequest -> { - handleRequest(wcEvent) - } - is WalletConnectEvents.PairConnectError -> { - handler.onPairConnectError(wcEvent.error) - } - } - } - .flowOn(dispatchers.io) - .collect() - } - - private suspend fun subscribeToSessions() { - sessions - .onEach { listOfSessions -> - val relevantTopics = getTopicsForUserWallet(userWalletId, sessionsRepository) - val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics) - handler.onListOfSessionsUpdated(filteredSessions) - } - .flowOn(dispatchers.io) - .collect() - } - - private fun filterSessionsForUserWallet( - availableSessions: List, - relevantTopics: List, - ): List { - return availableSessions.filter { relevantTopics.contains(it.topic) } - .map { - WcSessionForScreen( - description = it.name ?: "", - sessionId = it.topic, - ) - } - } - - private suspend fun getTopicsForUserWallet( - userWalletId: String, - repository: WalletConnectSessionsRepository, - ): List { - return repository.loadSessions(userWalletId).map { it.topic } - } - - fun approveSessionProposal(accounts: List) { - if (isNewWc) return - Timber.i("Approve session proposal: $accounts") - val userNamespaces: Map> = accounts - .groupBy { account -> - blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) - ?.let { NetworkNamespace(it) } - }.filterNotNull() - - val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId }) - .map { it.fullName } - walletConnectRepository.approve( - userNamespaces = userNamespaces, - blockchainNames = blockchainNames, - ) - } - - fun rejectSessionProposal() { - if (isNewWc) return - Timber.i("Reject session proposal") - walletConnectRepository.reject() - } - - fun disconnectSession(topic: String) { - if (isNewWc) return - Timber.i("Disconnect session: $topic") - walletConnectRepository.disconnect(topic) - } - - fun cancelRequest(topic: String, id: Long) { - if (isNewWc) return - Timber.i("Cancel request: $topic, $id") - walletConnectRepository.cancelRequest(topic, id) - } - - private suspend fun handleRequest(sessionRequest: WalletConnectEvents.SessionRequest) { - val error: WalletConnectError? = when { - sessionsRepository.loadSessions(userWalletId).none { it.topic == sessionRequest.topic } -> { - WalletConnectError.WrongUserWallet - } - sessionRequest.request is WcRequest.CustomRequest -> { - WalletConnectError.UnsupportedMethod - } - else -> { - null - } - } - val networkId = sessionRequest.chainId?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "" - val requestData = RequestData( - topic = sessionRequest.topic, - requestId = sessionRequest.id, - blockchain = networkId, - method = sessionRequest.method, - ) - - if (error != null) { - walletConnectRepository.rejectRequest(requestData, error) - return - } - - when (sessionRequest.request) { - is WcRequest.BnbCancel -> Unit - is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest( - requestData = requestData, - result = "", - ) - else -> { - currentRequest = sessionRequest - - val data = prepareRequestData(sessionRequest).getOrElse { e -> - val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError( - message = e.localizedMessage ?: "Unknown error", - ) - - walletConnectRepository.rejectRequest(requestData, wrappedError) - handler.onSessionRejected(wrappedError) - return - } - - handler.onSessionRequest(data) - } - } - } - - suspend fun continueWithRequest(request: WcPreparedRequest) { - if (isNewWc) return - val currentRequest = this.currentRequest - if (currentRequest == null || request.topic != currentRequest.topic) return - - val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return - val signingResultData = when (request) { - is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction( - data = request.preparedRequestData.data.data, - networkId = networkId, - derivationPath = request.derivationPath, - cardId = cardId, - ) - is WcPreparedRequest.EthTransaction -> sdkHelper.completeTransaction( - data = request.preparedRequestData, - cardId = cardId, - ) - is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage( - hashToSign = request.preparedRequestData.hash, - networkId = networkId, - type = request.preparedRequestData.type, - derivationPath = request.derivationPath, - cardId = cardId, - ) - is WcPreparedRequest.SolanaSignTransaction -> sdkHelper.signTransaction( - hashToSign = request.preparedRequestData.hashToSign, - networkId = networkId, - type = request.preparedRequestData.type, - derivationPath = request.derivationPath, - cardId = cardId, - ) - is WcPreparedRequest.SolanaSignMultipleTransactions -> sdkHelper.signTransactions( - hashesToSign = request.preparedRequestData.hashesToSign, - networkId = networkId, - type = request.preparedRequestData.type, - derivationPath = request.derivationPath, - cardId = cardId, - ) - } - - val requestData = RequestData( - topic = request.topic, - requestId = request.requestId, - blockchain = networkId, - method = currentRequest.method, - ) - - if (signingResultData == null) { - walletConnectRepository.rejectRequest(requestData, WalletConnectError.SigningError) - } else { - walletConnectRepository.sendRequest( - requestData = requestData, - result = signingResultData, - ) - } - } - - /** - * Handles Wallet Connect deep links. - * If wallet connect is able to handle the deeplink, session is started with deeplink. - * Otherwise, deeplink is stored until wallet connect is ready to handle it. - * - * @param deeplink deeplink to handle - */ - fun addDeeplink(deeplink: String) { - if (isNewWc) return - val deeplinkRegex = Regex(WC_PARAM_REGEX) - val matched = deeplinkRegex.findAll(deeplink) - val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull() - - val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session -> - session.topic == sessionTopic - } - - if (isAlreadyActiveSessionTopic && sessionTopic != null) { - Timber.i("WC already has an active session topic: $deeplink") - return - } - - if (isWalletConnectReadyForDeepLinks) { - val action = WalletConnectAction.OpenSession( - wcUri = deeplink, - source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, - userWalletId = UserWalletId(userWalletId), - ) - store.dispatchOnMain(action) - } else { - deeplinkStack.push(deeplink) - } - } - - private fun getCardId(userWallet: UserWallet): String? { - return if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.backupStatus?.isActive != true) { - userWallet.cardId - } else { // if wallet has backup, any card from wallet can be used to sign - null - } - } - - private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List): List { - val walletManagers = networks.mapNotNull { - val blockchain = it.toBlockchain() - - walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWallet.walletId, - blockchain = blockchain, - derivationPath = it.derivationPath.value, - ) - } - return walletManagers.flatMap { - val wallet = it.wallet - val chainIds = blockchainHelper.networkIdToChainIdOrNull(wallet.blockchain.toNetworkId()) - blockchainHelper.chainIdsToAccounts( - walletAddress = wallet.address, - chainIds = chainIds, - derivationPath = wallet.publicKey.derivationPath?.rawPath, - ) - } - } - - private suspend fun prepareRequestData( - sessionRequest: WalletConnectEvents.SessionRequest, - ): Result { - return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) - } - - private companion object { - const val WC_TOPIC_QUERY_NAME = "sessionTopic" - const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt deleted file mode 100644 index 74241e4cf6..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.walletconnect.model.legacy.Account - -interface WcBlockchainHelper { - fun chainIdToNetworkIdOrNull(chainId: String): String? - - fun chainIdToMissingNetworkNameOrNull(chainId: String): String? - - fun networkIdToChainIdOrNull(networkId: String): List - - fun getNamespaceFromFullChainIdOrNull(chainId: String): String? - - fun chainIdToFullNameOrNull(chainId: String): String? - - fun chainIdsToAccounts(walletAddress: String, chainIds: List, derivationPath: String?): List - - fun chainIdsToBlockchains(chainIds: List): List -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt deleted file mode 100644 index 3a79b205ed..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt +++ /dev/null @@ -1,297 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.squareup.moshi.* -import com.tangem.datasource.di.SdkMoshi -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam -import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage -import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -internal enum class WcJrpcMethods(val code: String) { - - ETH_SIGN("eth_sign"), - ETH_PERSONAL_SIGN("personal_sign"), - ETH_SIGN_TYPE_DATA("eth_signTypedData"), - ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"), - ETH_SIGN_TRANSACTION("eth_signTransaction"), - ETH_SEND_TRANSACTION("eth_sendTransaction"), - - BNB_SIGN("bnb_sign"), - BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"), - SIGN_TRANSACTION("trust_signTransaction"), - WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"), - - SOLANA_SIGN_TX("solana_signTransaction"), - SOLANA_SIGN_MESSAGE("solana_signMessage"), - SOLANA_SIGN_ALL_TX("solana_signAllTransactions"), - ; - - companion object { - fun fromCode(code: String): WcJrpcMethods? = values().firstOrNull { it.code == code } - } -} - -@JsonClass(generateAdapter = true) -data class WcSignTransaction( - @Json(name = "network") - val network: Int, - - @Json(name = "transaction") - val transaction: String, -) : WcRequestData - -@JsonClass(generateAdapter = true) -data class WcSignMessage( - @Json(name = "raw") - val raw: List, - - @Json(name = "type") - val type: WCSignType, -) : WcRequestData { - - @JsonClass(generateAdapter = false) - enum class WCSignType { - MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE, - } - - /** - * Raw parameters will always be the message and the address. Depending on the WCSignType, - * those parameters can be swapped as description below: - * - * - MESSAGE: `[address, data ]` - * - TYPED_MESSAGE: `[address, data]` - * - PERSONAL_MESSAGE: `[data, address]` - * - SOLANA_MESSAGE: `[publicKey (address), message]` - * - * reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata - */ - val data - get() = when (type) { - WCSignType.PERSONAL_MESSAGE -> raw[0] - else -> raw[1] - } - - val address - get() = when (type) { - WCSignType.PERSONAL_MESSAGE -> raw[1] - else -> raw[0] - } -} - -@JsonClass(generateAdapter = true) -data class WcSignMessageData( - @Json(name = "address") - val address: String, - - @Json(name = "message") - val message: String, -) : WcRequestData - -@JsonClass(generateAdapter = true) -data class WcAddChain( - @Json(name = "chainId") - val chainId: String, -) : WcRequestData - -@JsonClass(generateAdapter = true) -data class WcEthereumTransaction( - @Json(name = "from") - val from: String, - - @Json(name = "to") - val to: String?, - - @Json(name = "nonce") - val nonce: String?, - - @Json(name = "gasPrice") - val gasPrice: String?, - - @Json(name = "maxFeePerGas") - val maxFeePerGas: String?, - - @Json(name = "maxPriorityFeePerGas") - val maxPriorityFeePerGas: String?, - - @Json(name = "gas") - val gas: String?, - - @Json(name = "gasLimit") - val gasLimit: String?, - - @Json(name = "value") - val value: String?, - - @Json(name = "data") - val data: String, -) : WcRequestData - -@JsonClass(generateAdapter = true) -data class SolanaTransactionsRequest( - @Json(name = "transactions") - val transactions: List, -) : WcRequestData - -interface WcRequestData - -data class WcCustomRequestData(val data: String) : WcRequestData - -sealed class WcRequest(open val data: WcRequestData) { - data class EthSign(override val data: WcSignMessage) : WcRequest(data) - data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data) - data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data) - data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data) - data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data) - data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data) - data class BnbTxConfirm(override val data: WcBinanceTxConfirmParam) : WcRequest(data) - data class SignTransaction(override val data: WcSignTransaction) : WcRequest(data) - data class AddChain(override val data: WcAddChain) : WcRequest(data) - data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data) - data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data) - data class SolanaSignTransactions(override val data: SolanaTransactionsRequest) : WcRequest(data) -} - -@Singleton -internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) { - - @Suppress("ComplexMethod", "LongMethod") - fun deserialize(method: String, params: String): WcRequest { - val customRequest = WcRequest.CustomRequest(WcCustomRequestData(params)) - val wcMethod: WcJrpcMethods = WcJrpcMethods.fromCode(method) ?: return customRequest - - return when (wcMethod) { - WcJrpcMethods.ETH_SIGN_TRANSACTION -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java), - ).fromJsonFirstOrNull(params) ?: return customRequest - WcRequest.EthSignTransaction(data = deserializedParams) - } - WcJrpcMethods.ETH_SEND_TRANSACTION -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java), - ).fromJsonFirstOrNull(params) ?: return customRequest - WcRequest.EthSendTransaction(data = deserializedParams) - } - WcJrpcMethods.ETH_SIGN -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, String::class.java), - ).fromJsonOrNull(params) ?: return customRequest - val data = WcSignMessage( - raw = deserializedParams, - type = WcSignMessage.WCSignType.MESSAGE, - ) - WcRequest.EthSign(data = data) - } - WcJrpcMethods.ETH_PERSONAL_SIGN -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, String::class.java), - ).fromJsonOrNull(params) ?: return customRequest - val data = WcSignMessage( - raw = deserializedParams, - type = WcSignMessage.WCSignType.PERSONAL_MESSAGE, - ) - WcRequest.EthSign(data = data) - } - WcJrpcMethods.ETH_SIGN_TYPE_DATA, WcJrpcMethods.ETH_SIGN_TYPE_DATA_V4 -> { - val deserializedParams = listOf( - params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)), - params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1), - ) - val data = WcSignMessage( - deserializedParams, - WcSignMessage.WCSignType.TYPED_MESSAGE, - ) - Timber.d("TypedData params: $deserializedParams") - WcRequest.EthSign(data) - } - WcJrpcMethods.BNB_SIGN -> { - return deserializeBnb(moshi, params) ?: customRequest - } - WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcBinanceTxConfirmParam::class.java), - ).fromJsonFirstOrNull(params) ?: return customRequest - WcRequest.BnbTxConfirm(data = deserializedParams) - } - WcJrpcMethods.SIGN_TRANSACTION -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcSignTransaction::class.java), - ).fromJsonFirstOrNull(params) ?: return customRequest - - WcRequest.SignTransaction(data = deserializedParams) - } - WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> { - val deserializedParams: WcAddChain = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcAddChain::class.java), - ).fromJsonFirstOrNull(params) ?: return customRequest - - WcRequest.AddChain(data = deserializedParams) - } - WcJrpcMethods.SOLANA_SIGN_TX -> { - val tx = moshi.adapter(SolanaTransactionRequest::class.java) - .fromJsonOrNull(params) - ?: return customRequest - - WcRequest.SolanaSignRequest(data = tx) - } - WcJrpcMethods.SOLANA_SIGN_MESSAGE -> { - val signMessage = moshi.adapter(SolanaSignMessage::class.java) - .fromJsonOrNull(params) - ?: return customRequest - val data = WcSignMessage( - raw = listOf(signMessage.publicKey, signMessage.message), - type = WcSignMessage.WCSignType.SOLANA_MESSAGE, - ) - - WcRequest.EthSign(data = data) - } - WcJrpcMethods.SOLANA_SIGN_ALL_TX -> { - val transactionsToSign = moshi.adapter(SolanaTransactionsRequest::class.java) - .fromJsonOrNull(params) ?: return customRequest - WcRequest.SolanaSignTransactions(transactionsToSign) - } - } - } - - private fun deserializeBnb(moshi: Moshi, params: String): WcRequest? { - val cancelOrder = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcBinanceCancelOrder::class.java), - ).fromJsonFirstOrNull(params) - if (cancelOrder != null) return WcRequest.BnbCancel(cancelOrder) - - val tradeOrder = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcBinanceTradeOrder::class.java), - ).fromJsonFirstOrNull(params) - if (tradeOrder != null) return WcRequest.BnbTrade(tradeOrder) - - val transferOrder = moshi.adapter>( - Types.newParameterizedType(List::class.java, WcBinanceTransferOrder::class.java), - ).fromJsonFirstOrNull(params) - if (transferOrder != null) return WcRequest.BnbTransfer(transferOrder) - - return null - } - - private fun JsonAdapter.fromJsonOrNull(data: String): T? { - return try { - this.fromJson(data) - } catch (e: Exception) { - Timber.e(e.message) - null - } - } - - private fun JsonAdapter>.fromJsonFirstOrNull(data: String): T? { - return try { - this.fromJson(data)?.firstOrNull() - } catch (e: Exception) { - Timber.e(e.message) - null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt deleted file mode 100644 index ba9f21b2d6..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.tap.domain.walletconnect2.domain.models.BnbData -import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData - -sealed class WcPreparedRequest( - open val preparedRequestData: Any, - val topic: String, - val requestId: Long, - val derivationPath: String?, -) { - class EthSign( - override val preparedRequestData: WcPersonalSignData, - topic: String, - requestId: Long, - derivationPath: String?, - ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) - - class EthTransaction( - override val preparedRequestData: WcTransactionData, - topic: String, - requestId: Long, - derivationPath: String?, - ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) - - class BnbTransaction( - override val preparedRequestData: BnbData, - topic: String, - requestId: Long, - derivationPath: String?, - ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) - - class SolanaSignTransaction( - override val preparedRequestData: GenericTransactionData.SingleHash, - topic: String, - requestId: Long, - derivationPath: String?, - ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) - - class SolanaSignMultipleTransactions( - override val preparedRequestData: GenericTransactionData.MultipleHashes, - topic: String, - requestId: Long, - derivationPath: String?, - ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) -} - -sealed interface GenericTransactionData { - val dAppName: String - val type: TransactionType - - data class SingleHash( - val hashToSign: ByteArray, - override val dAppName: String, - override val type: TransactionType, - ) : GenericTransactionData - - data class MultipleHashes( - val hashesToSign: List, - override val dAppName: String, - override val type: TransactionType, - ) : GenericTransactionData -} - -enum class TransactionType { - SOLANA_TX, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt deleted file mode 100644 index 90de1f8b12..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper -import com.tangem.tap.domain.walletconnect2.domain.models.BnbData -import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType -import okio.ByteString.Companion.decodeBase64 - -internal class WcSessionRequestConverter( - private val blockchainHelper: WcBlockchainHelper, - private val sessionsRepository: WalletConnectSessionsRepository, - private val sdkHelper: WalletConnectSdkHelper, -) { - - @Suppress("LongMethod") - suspend fun prepareRequest( - sessionRequest: WalletConnectEvents.SessionRequest, - userWalletId: String, - ): Result = runCatching { - val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) { - "Failed to get network ID for chain ID: ${sessionRequest.chainId}" - } - val derivationPath = getDerivationPath( - sessionsRepository = sessionsRepository, - sessionRequest = sessionRequest, - userWalletId = userWalletId, - walletAddress = getWalletAddress(sessionRequest.request), - ) - - when (val request = sessionRequest.request) { - is WcRequest.EthSendTransaction -> { - val data = sdkHelper.prepareTransactionData( - EthTransactionData( - transaction = request.data, - networkId = networkId, - rawDerivationPath = derivationPath, - id = sessionRequest.id, - topic = sessionRequest.topic, - type = WcEthTransactionType.EthSendTransaction, - metaName = sessionRequest.metaName, - metaUrl = sessionRequest.metaUrl, - ), - ) - - WcPreparedRequest.EthTransaction( - preparedRequestData = data, - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.EthSignTransaction -> { - val data = sdkHelper.prepareTransactionData( - EthTransactionData( - transaction = request.data, - networkId = networkId, - rawDerivationPath = derivationPath, - id = sessionRequest.id, - topic = sessionRequest.topic, - type = WcEthTransactionType.EthSignTransaction, - metaName = sessionRequest.metaName, - metaUrl = sessionRequest.metaUrl, - ), - ) - - WcPreparedRequest.EthTransaction( - preparedRequestData = data, - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.EthSign -> { - val data = sdkHelper.prepareDataForPersonalSign( - request.data, - sessionRequest.topic, - sessionRequest.metaName, - sessionRequest.id, - ) - WcPreparedRequest.EthSign( - preparedRequestData = data, - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.BnbTrade -> { - val data = sdkHelper.prepareBnbTradeOrder(request.data) - WcPreparedRequest.BnbTransaction( - BnbData( - data = data, - topic = sessionRequest.topic, - requestId = sessionRequest.id, - dAppName = sessionRequest.metaName, - ), - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.BnbTransfer -> { - val data = sdkHelper.prepareBnbTransferOrder(request.data) - WcPreparedRequest.BnbTransaction( - BnbData( - data = data, - topic = sessionRequest.topic, - requestId = sessionRequest.id, - dAppName = sessionRequest.metaName, - ), - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.SolanaSignRequest -> { - val transaction = request.data.transaction - - WcPreparedRequest.SolanaSignTransaction( - preparedRequestData = GenericTransactionData.SingleHash( - hashToSign = transaction.prepareSolanaTransaction(), - dAppName = sessionRequest.metaName, - type = TransactionType.SOLANA_TX, - ), - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - is WcRequest.SolanaSignTransactions -> { - val transactions = request.data.transactions - WcPreparedRequest.SolanaSignMultipleTransactions( - preparedRequestData = GenericTransactionData.MultipleHashes( - hashesToSign = transactions.map { it.prepareSolanaTransaction() }, // - dAppName = sessionRequest.metaName, - type = TransactionType.SOLANA_TX, - ), - topic = sessionRequest.topic, - requestId = sessionRequest.id, - derivationPath = derivationPath, - ) - } - else -> throw WalletConnectError.UnsupportedMethod - } - } - - /** - * Input transaction in Base64 string - */ - private fun String.prepareSolanaTransaction(): ByteArray { - val transaction = this.decodeBase64()?.toByteArray() ?: ByteArray(0) - return SolanaTransactionHelper.removeSignaturesPlaceholders(transaction) - } - - private fun getWalletAddress(request: WcRequest): String? { - return when (request) { - is WcRequest.BnbTrade -> request.data.accountNumber - is WcRequest.BnbTransfer -> request.data.accountNumber - is WcRequest.EthSendTransaction -> request.data.from - is WcRequest.EthSignTransaction -> request.data.from - is WcRequest.EthSign -> request.data.address - is WcRequest.SolanaSignRequest -> request.data.feePayer - else -> null - } - } - - private suspend fun getDerivationPath( - sessionsRepository: WalletConnectSessionsRepository, - sessionRequest: WalletConnectEvents.SessionRequest, - userWalletId: String, - walletAddress: String?, - ): String? { - return sessionsRepository.loadSessions(userWalletId) - .firstOrNull { it.topic == sessionRequest.topic } - ?.accounts?.firstOrNull { - // if walletAddress == null take first account - it.chainId == sessionRequest.chainId && - (walletAddress == null || it.walletAddress.lowercase() == walletAddress.lowercase()) - }?.derivationPath - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt deleted file mode 100644 index 194ca5d01d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData - -data class BnbData( - val data: BinanceMessageData, - val topic: String, - val requestId: Long, - val dAppName: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt deleted file mode 100644 index 2138d6bd63..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType - -data class EthTransactionData( - val transaction: WcEthereumTransaction, - val networkId: String, - val rawDerivationPath: String?, - val id: Long, - val topic: String, - val type: WcEthTransactionType, - val metaName: String, - val metaUrl: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt deleted file mode 100644 index a431210e42..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -@JvmInline -value class NetworkNamespace(val value: String) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt deleted file mode 100644 index 38cd19ca97..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -data class RequestData( - val topic: String, - val requestId: Long, - val method: String, - val blockchain: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt deleted file mode 100644 index 40d82cdb51..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -sealed class WalletConnectError(val error: String) : Exception() { - - data object UnsupportedDApp : WalletConnectError("UnsupportedDApp") - - data class ApprovalErrorMissingNetworks( - val missingChains: List, - ) : WalletConnectError("ApprovalErrorMissingNetworks") - - data class ApprovalErrorAddNetwork( - val networks: List, - ) : WalletConnectError("ApprovalErrorAddNetwork") - - data class ApprovalErrorUnsupportedNetwork( - val unsupportedNetworks: List, - ) : WalletConnectError("ApprovalErrorUnsupportedNetwork") - - data class ExternalApprovalError( - override val message: String?, - ) : WalletConnectError("ExternalApprovalError") - - data class UnknownError( - override val message: String, - ) : WalletConnectError(message) - - data object WrongUserWallet : WalletConnectError("WrongUserWallet") - data object UnsupportedMethod : WalletConnectError("UnsupportedMethod") - data object SigningError : WalletConnectError("SigningError") - data object ValidationError : WalletConnectError("ValidationError") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt deleted file mode 100644 index 1298ede44d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -import com.tangem.domain.walletconnect.model.legacy.Account -import com.tangem.tap.domain.walletconnect2.domain.WcRequest -import java.net.URI - -sealed interface WalletConnectEvents { - data class SessionProposal( - val name: String, - val description: String, - val url: String, - val icons: List, - val requiredChainIds: List, - val optionalChainIds: List, - ) : WalletConnectEvents - - data class SessionApprovalError(val error: WalletConnectError) : WalletConnectEvents - data class SessionApprovalSuccess(val topic: String, val accounts: List) : WalletConnectEvents - data class SessionDeleted(val topic: String) : WalletConnectEvents - - data class SessionRequest( - val request: WcRequest, - val chainId: String?, - val topic: String, - val id: Long, - val metaName: String, - val metaUrl: String, - val method: String, - ) : WalletConnectEvents - - data class PairConnectError(val error: Throwable) : WalletConnectEvents -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt deleted file mode 100644 index fb535bcc66..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models - -data class WalletConnectSession( - val topic: String, - val icon: String?, - val name: String?, - val url: String?, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt deleted file mode 100644 index 112c92e7a6..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.binance - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@Suppress("LongParameterList") -@JsonClass(generateAdapter = true) -data class WcBinanceCancelOrder( - @Json(name = "account_number") - val accountNumber: String, - @Json(name = "chain_id") - val chainId: String, - @Json(name = "data") - val data: String?, - @Json(name = "memo") - val memo: String?, - @Json(name = "sequence") - val sequence: String, - @Json(name = "source") - val source: String, - @Json(name = "msgs") - val msgs: List, -) : WcRequestData { - - @JsonClass(generateAdapter = true) - data class Message( - @Json(name = "refid") - val refid: String, - @Json(name = "sender") - val sender: String, - @Json(name = "symbol") - val symbol: String, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt deleted file mode 100644 index bdeaba5b27..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.binance - -import com.github.salomonbrys.kotson.jsonSerializer -import com.google.gson.JsonObject -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@Suppress("LongParameterList") -@JsonClass(generateAdapter = true) -data class WcBinanceTradeOrder( - @Json(name = "account_number") - val accountNumber: String, - @Json(name = "chain_id") - val chainId: String, - @Json(name = "data") - val data: String?, - @Json(name = "memo") - val memo: String?, - @Json(name = "sequence") - val sequence: String, - @Json(name = "source") - val source: String, - @Json(name = "msgs") - val msgs: List, -) : WcRequestData { - - @JsonClass(generateAdapter = false) - enum class MessageKey(val key: String) { - ID("id"), - ORDER_TYPE("ordertype"), - PRICE("price"), - QUANTITY("quantity"), - SENDER("sender"), - SIDE("side"), - SYMBOL("symbol"), - TIME_INFORCE("timeinforce"), - } - - @JsonClass(generateAdapter = true) - data class Message( - @Json(name = "id") - val id: String, - @Json(name = "orderType") - val orderType: Int, - @Json(name = "price") - val price: Long, - @Json(name = "quantity") - val quantity: Long, - @Json(name = "sender") - val sender: String, - @Json(name = "side") - val side: Int, - @Json(name = "symbol") - val symbol: String, - @Json(name = "timeInforce") - val timeInforce: Int, - ) -} - -val tradeOrderSerializer = jsonSerializer { - val jsonObject = JsonObject() - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ID.key, it.src.id) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ORDER_TYPE.key, it.src.orderType) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.PRICE.key, it.src.price) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.QUANTITY.key, it.src.quantity) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SENDER.key, it.src.sender) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SIDE.key, it.src.side) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SYMBOL.key, it.src.symbol) - jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.TIME_INFORCE.key, it.src.timeInforce) - - jsonObject -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt deleted file mode 100644 index 99199c17dc..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.binance - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@Suppress("LongParameterList") -@JsonClass(generateAdapter = true) -data class WcBinanceTransferOrder( - @Json(name = "account_number") - val accountNumber: String, - @Json(name = "chain_id") - val chainId: String, - @Json(name = "data") - val data: String?, - @Json(name = "memo") - val memo: String?, - @Json(name = "sequence") - val sequence: String, - @Json(name = "source") - val source: String, - @Json(name = "msgs") - val msgs: List, -) : WcRequestData { - - @JsonClass(generateAdapter = true) - data class Message( - @Json(name = "inputs") - val inputs: List, - @Json(name = "outputs") - val outputs: List, - ) { - - @JsonClass(generateAdapter = true) - data class Item( - @Json(name = "address") - val address: String, - @Json(name = "coins") - val coins: List, - ) { - - @JsonClass(generateAdapter = true) - data class Coin( - @Json(name = "amount") - val amount: Long, - @Json(name = "denom") - val denom: String, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt deleted file mode 100644 index 24ada4ca98..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.binance - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@JsonClass(generateAdapter = true) -data class WcBinanceTxConfirmParam( - @Json(name = "ok") - val ok: Boolean, - @Json(name = "errorMsg") - val errorMsg: String?, -) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt deleted file mode 100644 index a521aa55e9..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.solana - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -internal data class SolanaSignMessage( - @Json(name = "pubkey") - val publicKey: String, - - @Json(name = "message") - val message: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt deleted file mode 100644 index bed9fd3897..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.solana - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@JsonClass(generateAdapter = true) -data class SolanaTransactionRequest( - @Json(name = "feePayer") - val feePayer: String?, - - @Json(name = "transaction") - val transaction: String, -) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt deleted file mode 100644 index 03cb9b878d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.toggles - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles - -internal class DefaultWalletConnectFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : WalletConnectFeatureToggles { - - override val isRedesignedWalletConnectEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 0ba699bde1..e4070d4b60 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -1,44 +1,22 @@ package com.tangem.tap.features.demo -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import com.tangem.wallet.R -import org.rekotlin.Action object DemoHelper { val config = DemoConfig() - private val disabledActionFeatures = listOf( - WalletConnectAction.StartWalletConnect::class.java, - ) - fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId) fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId) - fun tryHandle(appState: () -> AppState?, action: Action): Boolean { + fun tryHandle(appState: () -> AppState?): Boolean { val scanResponse = getScanResponse(appState) ?: return false if (!scanResponse.isDemoCard()) return false - disabledActionFeatures.firstOrNull { it == action::class.java }?.let { - val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender) - uiMessageSender.send( - DialogMessage( - message = resourceReference(R.string.alert_demo_feature_disabled), - ), - ) - return true - } - return false } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 9281cbc507..2ebe0494ab 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -39,7 +39,7 @@ class DetailsMiddleware { val detailsMiddleware: Middleware = { _, stateProvider -> { next -> { action -> - if (!DemoHelper.tryHandle(stateProvider, action)) { + if (!DemoHelper.tryHandle(stateProvider)) { val detailsState = stateProvider()?.detailsState if (detailsState != null) { handleAction(detailsState, action) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt deleted file mode 100644 index bc90c602d0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.tap.features.details.redux.walletconnect - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen -import org.rekotlin.Action - -sealed class WalletConnectAction : Action { - data class HandleDeepLink(val wcUri: String?) : WalletConnectAction() - - data object StartWalletConnect : WalletConnectAction() - - data class OpenSession( - val wcUri: String, - val source: SourceType, - val userWalletId: UserWalletId, - ) : WalletConnectAction() { - enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC } - } - - data class DisconnectSession(val topic: String) : WalletConnectAction() - - data class RejectRequest(val topic: String, val id: Long) : WalletConnectAction() - - //region WalletConnect 2.0 - data class ApproveProposal(val proposal: WalletConnectEvents.SessionProposal) : WalletConnectAction() - data object RejectProposal : WalletConnectAction() - - data object SessionEstablished : WalletConnectAction() - data class SessionRejected(val error: WalletConnectError) : WalletConnectAction() - data class SessionListUpdated(val sessions: List) : WalletConnectAction() - - data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction() - - data object RejectUnsupportedRequest : WalletConnectAction() - - data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction() - - data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction() - - data object UnsupportedDappRequest : WalletConnectAction() - //endregion WalletConnect 2.0 -} \ No newline at end of file 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 deleted file mode 100644 index 499e1861b0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ /dev/null @@ -1,202 +0,0 @@ -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 -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import timber.log.Timber - -@Suppress("LargeClass") -class WalletConnectMiddleware { - private val walletConnectInteractor: WalletConnectInteractor - get() = store.inject(DaggerGraphState::walletConnectInteractor) - private val walletConnectRepository: LegacyWalletConnectRepository - get() = store.inject(DaggerGraphState::walletConnectRepository) - - val walletConnectMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - handle(state, action) - next(action) - } - } - } - - @Suppress("ComplexMethod", "LongMethod") - private fun handle(state: () -> AppState?, action: Action) { - if (DemoHelper.tryHandle(state, action)) return - - when (action) { - is WalletConnectAction.HandleDeepLink -> { - val wsUrl = action.wcUri - Timber.i("WC deeplink: $wsUrl") - if (!wsUrl.isNullOrBlank()) { - Timber.i("WC deeplink added to stack: $wsUrl") - walletConnectInteractor.addDeeplink(wsUrl) - } - } - is WalletConnectAction.DisconnectSession -> { - walletConnectInteractor.disconnectSession(action.topic) - } - is WalletConnectAction.StartWalletConnect -> { - store.dispatchNavigationAction { - push(AppRoute.QrScanning(AppRoute.QrScanning.Source.WalletConnect)) - } - } - is WalletConnectAction.OpenSession -> { - val index = action.wcUri.indexOf("@") - when (action.wcUri[index + 1]) { - '2' -> walletConnectRepository.pair( - uri = action.wcUri, - source = action.source, - userWalletId = action.userWalletId, - ) - '1' -> { - store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) - store.dispatchOnMain( - GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedWcVersion), - ) - } - } - } - is WalletConnectAction.RejectRequest -> { - walletConnectInteractor.cancelRequest(action.topic, action.id) - } - is WalletConnectAction.ApproveProposal -> { - scope.launch { - val proposalChainIds = action.proposal.requiredChainIds + action.proposal.optionalChainIds - val proposalBlockchains = - walletConnectInteractor.blockchainHelper.chainIdsToBlockchains(proposalChainIds) - val accounts = getWalletManagers() - .filter { walletManager -> proposalBlockchains.contains(walletManager.wallet.blockchain) } - .flatMap { - val wallet = it.wallet - val chainIds = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), - ) - walletConnectInteractor.blockchainHelper.chainIdsToAccounts( - walletAddress = wallet.address, - chainIds = chainIds, - derivationPath = wallet.publicKey.derivationPath?.rawPath, - ) - } - walletConnectInteractor.approveSessionProposal(accounts) - } - } - is WalletConnectAction.RejectProposal -> { - walletConnectInteractor.rejectSessionProposal() - } - is WalletConnectAction.SessionEstablished -> { - } - is WalletConnectAction.SessionRejected -> { - when (action.error) { - is WalletConnectError.ApprovalErrorAddNetwork -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(action.error.networks), - ), - ) - } - is WalletConnectError.ApprovalErrorUnsupportedNetwork -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.UnsupportedNetwork(action.error.unsupportedNetworks), - ), - ) - } - is WalletConnectError.UnsupportedDApp -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.UnsupportedDapp, - ), - ) - } - is WalletConnectError.UnknownError -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - AppDialog.SimpleOkDialogRes( - headerId = R.string.wallet_connect_title, - messageId = R.string.wallet_connect_error_with_framework_message, - args = listOf(action.error.message), - ), - ), - ) - } - is WalletConnectError.ExternalApprovalError -> { - Timber.e(action.error, "ExternalApprovalError ${action.error.message}") - // do not show dialog on this event - // val message = action.error.message - // if (!message.isNullOrEmpty()) { - // store.dispatchOnMain( - // GlobalAction.ShowDialog( - // AppDialog.SimpleOkWarningDialog( - // message = message, - // ), - // ), - // ) - // } - } - else -> Unit - } - } - - is WalletConnectAction.ShowSessionRequest -> { - val dialog: WalletConnectDialog = when (val request = action.sessionRequest) { - is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request) - is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request) - is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request) - is WcPreparedRequest.SolanaSignTransaction -> - WalletConnectDialog.SignTransactionDialog(request) - is WcPreparedRequest.SolanaSignMultipleTransactions -> - WalletConnectDialog.SignTransactionsDialog(request) - } - store.dispatch(GlobalAction.ShowDialog(dialog)) - } - is WalletConnectAction.PerformRequestedAction -> { - scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) } - } - is WalletConnectAction.RejectUnsupportedRequest -> { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) - } - is WalletConnectAction.PairConnectErrorAction -> { - store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable))) - } - } - } - - private suspend fun getWalletManagers(): List { - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - 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/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt deleted file mode 100644 index 1cf6c4da24..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.tap.features.details.redux.walletconnect - -import org.rekotlin.Action - -object WalletConnectReducer { - fun reduce(action: Action, state: WalletConnectState): WalletConnectState { - if (action !is WalletConnectAction) return state - - return when (action) { - is WalletConnectAction.OpenSession -> { - state.copy(loading = true) - } - is WalletConnectAction.ApproveProposal -> state.copy(loading = true) - is WalletConnectAction.RejectProposal, - is WalletConnectAction.SessionEstablished, - is WalletConnectAction.SessionRejected, - is WalletConnectAction.PairConnectErrorAction, - is WalletConnectAction.UnsupportedDappRequest, - -> state.copy(loading = false) - is WalletConnectAction.SessionListUpdated -> state.copy( - wc2Sessions = action.sessions, - ) - else -> state - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt deleted file mode 100644 index ac28ca0b95..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.tangem.tap.features.details.redux.walletconnect - -import com.squareup.moshi.JsonClass -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen -import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData -import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData - -data class WalletConnectState( - val loading: Boolean = false, - val wc2Sessions: List = listOf(), - val newSessionData: NewWcSessionData? = null, -) - -data class NewWcSessionData( - val session: WalletConnectSession, - val scanResponse: ScanResponse, - val blockchain: Blockchain?, -) - -data class WalletConnectSession( - val peerId: String, - val remotePeerId: String?, - val wallet: WalletForSession, -) { - fun getAddress(): String? { - val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null - return wallet.blockchain?.makeAddresses(key)?.first()?.value - } -} - -@JsonClass(generateAdapter = true) -data class WalletForSession( - val walletPublicKey: ByteArray? = null, - val derivedPublicKey: ByteArray? = null, - val derivationPath: DerivationPath? = null, - val derivationStyle: DerivationStyle? = null, - val isTestNet: Boolean = false, - val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum, -) { - - fun getBlockchainForSession(): Blockchain { - return blockchain ?: if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as WalletForSession - - if (walletPublicKey != null) { - if (other.walletPublicKey == null) return false - if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false - } else if (other.walletPublicKey != null) return false - if (derivedPublicKey != null) { - if (other.derivedPublicKey == null) return false - if (!derivedPublicKey.contentEquals(other.derivedPublicKey)) return false - } else if (other.derivedPublicKey != null) return false - if (derivationPath != other.derivationPath) return false - if (isTestNet != other.isTestNet) return false - return blockchain == other.blockchain - } - - override fun hashCode(): Int { - var result = walletPublicKey?.contentHashCode() ?: 0 - result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0) - result = 31 * result + (derivationPath?.hashCode() ?: 0) - result = 31 * result + isTestNet.hashCode() - result = 31 * result + (blockchain?.hashCode() ?: 0) - return result - } -} - -sealed class WalletConnectDialog : StateDialog { - data object UnsupportedWcVersion : WalletConnectDialog() - data object UnsupportedCard : WalletConnectDialog() - data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog() - data object UnsupportedDapp : WalletConnectDialog() - data class AddNetwork(val networks: List) : WalletConnectDialog() - data object OpeningSessionRejected : WalletConnectDialog() - data object SessionTimeout : WalletConnectDialog() - data class ApproveWcSession( - val session: WalletConnectSession, - val networks: List, - ) : WalletConnectDialog() - - data class SessionProposalDialog( - val sessionProposal: WalletConnectEvents.SessionProposal, - val networks: String, - val onApprove: () -> Unit, - val onReject: () -> Unit, - ) : WalletConnectDialog() - - data class ChooseNetwork( - val session: WalletConnectSession, - val networks: List, - ) : WalletConnectDialog() - - data class RequestTransaction(val data: WcPreparedRequest.EthTransaction) : - WalletConnectDialog() - - data class PersonalSign(val data: WcPreparedRequest.EthSign) : WalletConnectDialog() - data class BnbTransactionDialog( - val data: WcPreparedRequest.BnbTransaction, - ) : WalletConnectDialog() - - data class SignTransactionDialog( - val data: WcPreparedRequest.SolanaSignTransaction, - ) : WalletConnectDialog() - - data class SignTransactionsDialog( - val data: WcPreparedRequest.SolanaSignMultipleTransactions, - ) : WalletConnectDialog() - - data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog() -} - -data class WcTransactionData( - val type: WcEthTransactionType, - val transaction: TransactionData, - val topic: String, - val id: Long, - val walletManager: WalletManager, - val dialogData: TransactionRequestDialogData, -) - -enum class WcEthTransactionType { - EthSignTransaction, - EthSendTransaction, -} - -data class WcPersonalSignData( - val hash: ByteArray, - val topic: String, - val id: Long, - val dialogData: PersonalSignDialogData, - val type: WcSignMessage.WCSignType, -) - -sealed class BinanceMessageData( - val address: String, - val data: ByteArray, -) { - class Trade( - val tradeData: List, - address: String, - data: ByteArray, - ) : BinanceMessageData(address, data) - - class Transfer( - val outputAddress: String, - val amount: String, - address: String, - data: ByteArray, - ) : BinanceMessageData(address, data) -} - -data class TradeData( - val price: String, - val quantity: String, - val amount: String, - val symbol: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt deleted file mode 100644 index 0974e0db1b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -import com.tangem.tap.store -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import org.rekotlin.StoreSubscriber - -@Suppress("UnusedPrivateMember") -internal class DefaultWalletConnectComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: WalletConnectComponent.Params, -) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber { - - private val model: WalletConnectModel = getOrCreateModel(params) - - private var screenState: MutableState = - mutableStateOf(model.updateState(store.state.walletConnectState)) - - init { - lifecycle.subscribe( - onCreate = { - Analytics.send(WalletConnect.ScreenOpened()) - }, - onStart = { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.walletConnectState == newState.walletConnectState - }.select { it.walletConnectState } - } - }, - onStop = { - store.unsubscribe(this) - }, - ) - } - - override fun newState(state: WalletConnectState) { - screenState.value = model.updateState(state) - } - - @Composable - override fun Content(modifier: Modifier) { - WalletConnectScreen( - modifier = modifier, - state = screenState.value, - onBackClick = { - store.dispatchNavigationAction(AppRouter::pop) - }, - ) - } - - @AssistedFactory - interface Factory : WalletConnectComponent.Factory { - override fun create( - context: AppComponentContext, - params: WalletConnectComponent.Params, - ): DefaultWalletConnectComponent - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt deleted file mode 100644 index a231426ebb..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect - -import androidx.compose.runtime.Stable -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.domain.qrscanning.models.QrResultSource -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Stable -@ModelScoped -internal class WalletConnectModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val listenToQrScanningUseCase: ListenToQrScanningUseCase, - paramsContainer: ParamsContainer, -) : Model() { - - private val params = paramsContainer.require() - - init { - modelScope.launch { - listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT) - .getOrElse { emptyFlow() } - .map { result -> - val source = when (result.resultSource) { - QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD - QrResultSource.CAMERA, - QrResultSource.GALLERY, - -> WalletConnectAction.OpenSession.SourceType.QR - } - WalletConnectAction.OpenSession( - wcUri = result.qrCode, - source = source, - userWalletId = params.userWalletId, - ) - } - .collect { store.dispatch(it) } - } - } - - fun updateState(state: WalletConnectState): WalletConnectScreenState { - Timber.d("WC2 Sessions: ${state.wc2Sessions}") - val sessions = state.wc2Sessions - return WalletConnectScreenState( - sessions.toImmutableList(), - isLoading = state.loading, - onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) }, - onAddSession = { - store.dispatch(WalletConnectAction.StartWalletConnect) - }, - ) - } - - private fun onRemoveSession(sessionUri: String, wc2sessions: List) { - wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let { - store.dispatch(WalletConnectAction.DisconnectSession(sessionUri)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt deleted file mode 100644 index 252fc23b51..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect - -import androidx.compose.foundation.Image -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.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold -import com.tangem.wallet.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletConnectScreen( - state: WalletConnectScreenState, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - SettingsScreensScaffold( - modifier = modifier, - content = { - if (state.sessions.isEmpty()) { - EmptyScreen(state) - } else { - WalletConnectSessions(state) - } - }, - fab = { - if (!state.isLoading) { - AddSessionFab( - onAddSession = { - Analytics.send(Settings.ButtonStartWalletConnectSession()) - state.onAddSession() - }, - ) - } - }, - titleRes = R.string.wallet_connect_title, - onBackClick = onBackClick, - ) -} - -@Composable -private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) { - FloatingActionButton( - onClick = onAddSession, - containerColor = TangemTheme.colors.button.primary, - contentColor = TangemTheme.colors.icon.primary2, - shape = RoundedCornerShape(16.dp), - modifier = modifier, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_plus_24), - contentDescription = "", - ) - } -} - -@Composable -private fun EmptyScreen(state: WalletConnectScreenState) { - if (state.isLoading) { - TangemLinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = TangemTheme.colors.icon.accent, - ) - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 64.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(id = R.drawable.ic_wallet_connect_24), - contentDescription = "", - colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), - contentScale = ContentScale.FillWidth, - modifier = Modifier.width(width = 100.dp), - ) - Spacer(modifier = Modifier.size(24.dp)) - Text( - text = stringResourceSafe(id = R.string.wallet_connect_subtitle), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -private fun WalletConnectSessions(state: WalletConnectScreenState) { - if (state.isLoading) { - TangemLinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(2.dp), - color = TangemTheme.colors.icon.accent, - ) - } else { - Spacer(modifier = Modifier.height(2.dp)) - } - - LazyColumn( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items(state.sessions) { session -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = session.description, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.weight(1f), - ) - IconButton( - onClick = { - Analytics.send(Settings.ButtonStopWalletConnectSession()) - state.onRemoveSession(session.sessionId) - }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_cross_rounded_24), - contentDescription = "", - tint = TangemTheme.colors.icon.warning, - ) - } - } - } - } -} - -@Composable -@Preview -private fun WalletConnectScreenPreview() { - WalletConnectScreen( - state = WalletConnectScreenState( - sessions = persistentListOf( - WcSessionForScreen( - description = "session from some dApp", - sessionId = "", - ), - ), - isLoading = true, - ), - {}, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt deleted file mode 100644 index 0a3d429cd6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect - -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletConnectScreenState( - val sessions: ImmutableList, - val isLoading: Boolean = false, - val onRemoveSession: (String) -> Unit = {}, - val onAddSession: () -> Unit = {}, -) - -data class WcSessionForScreen( - val description: String, - val sessionId: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt deleted file mode 100644 index 2c97e35722..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.wallet.UserWalletId - -interface WalletConnectComponent : ComposableContentComponent { - data class Params(val userWalletId: UserWalletId) - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/di/WalletConnectFeatureModule.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/di/WalletConnectFeatureModule.kt deleted file mode 100644 index 20a85d18be..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/di/WalletConnectFeatureModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.di - -import com.tangem.core.decompose.model.Model -import com.tangem.tap.features.details.ui.walletconnect.DefaultWalletConnectComponent -import com.tangem.tap.features.details.ui.walletconnect.WalletConnectModel -import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -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 WalletConnectFeatureModule { - - @Binds - fun bindFactory(impl: DefaultWalletConnectComponent.Factory): WalletConnectComponent.Factory - - @Binds - @IntoMap - @ClassKey(WalletConnectModel::class) - fun bindModel(model: WalletConnectModel): Model -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt deleted file mode 100644 index aee88834e4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object BnbTransactionDialog { - fun create(preparedData: WcPreparedRequest.BnbTransaction, context: Context): AlertDialog { - val data = preparedData.preparedRequestData.data - val message = when (data) { - is BinanceMessageData.Trade -> data.tradeData.map { - context.getString( - R.string.wallet_connect_bnb_trade_order_message, - it.symbol, - it.price, - it.quantity, - it.amount, - ) - }.joinToString(separator = "\n\n") - is BinanceMessageData.Transfer -> context.getString( - R.string.wallet_connect_bnb_transaction_message, - data.address, - data.outputAddress, - data.amount, - ) - } - - val fullMessage = context.getString( - R.string.wallet_connect_bnb_sign_message, - preparedData.preparedRequestData.dAppName, - message, - ) - val positiveButtonTitle = context.getText(R.string.common_sign) - - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(fullMessage) - setPositiveButton(positiveButtonTitle) { _, _ -> - store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt deleted file mode 100644 index 4403b6d4a6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object PersonalSignDialog { - fun create(preparedData: WcPreparedRequest.EthSign, context: Context): AlertDialog { - val data = preparedData.preparedRequestData.dialogData - val message = context.getString(R.string.wallet_connect_alert_sign_message, data.message) - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} - -data class PersonalSignDialogData( - val dAppName: String, - val message: String, - val topic: String, - val id: Long, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt deleted file mode 100644 index 1dd8aff5a7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.store -import com.tangem.wallet.R - -object SessionProposalDialog { - fun create( - sessionProposal: WalletConnectEvents.SessionProposal, - networks: String, - context: Context, - onApprove: () -> Unit, - onReject: () -> Unit, - ): AlertDialog { - val message = context.getString( - R.string.wallet_connect_request_session_start, - sessionProposal.name, - networks, - sessionProposal.url, - ) - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_start)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - onApprove() - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - onReject() - } - setOnCancelListener { - store.dispatch(GlobalAction.HideDialog) - onReject() - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt deleted file mode 100644 index e459b6065c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.store -import com.tangem.wallet.R - -internal object SignTransactionDialog { - - fun create(preparedData: WcPreparedRequest.SolanaSignTransaction, context: Context): AlertDialog { - val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "") - val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage" - return AlertDialog.Builder(context).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionsDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionsDialog.kt deleted file mode 100644 index 579940d0ce..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionsDialog.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.store -import com.tangem.wallet.R - -internal object SignTransactionsDialog { - - fun create(preparedData: WcPreparedRequest.SolanaSignMultipleTransactions, context: Context): AlertDialog { - val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "") - val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage" - return AlertDialog.Builder(context).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt deleted file mode 100644 index 4fd49641fb..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType -import com.tangem.tap.store -import com.tangem.wallet.R - -object TransactionDialog { - fun create(preparedData: WcPreparedRequest.EthTransaction, context: Context): AlertDialog { - val data = preparedData.preparedRequestData.dialogData - val message = context.getString( - R.string.wallet_connect_create_tx_message, - data.dAppName, - data.dAppUrl, - data.amount, - data.feeAmount, - data.totalAmount, - data.balance, - ) - - val positiveButtonTitle = when (data.type) { - WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign) - WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send) - } - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(positiveButtonTitle) { _, _ -> - if (data.isEnoughFundsToSend) { - store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) - } else { - store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) - } - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} - -data class TransactionRequestDialogData( - val dAppName: String, - val dAppUrl: String, - val amount: String, - val feeAmount: String, - val totalAmount: String, - val balance: String, - val isEnoughFundsToSend: Boolean, - val topic: String, - val id: Long, - val type: WcEthTransactionType, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt deleted file mode 100644 index e715290a1e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.tap.features.intentHandler.handlers - -import android.content.Intent -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.removePrefixOrNull -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.intentHandler.AffectsNavigation -import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.store -import timber.log.Timber -import java.net.URLDecoder - -/** -[REDACTED_AUTHOR] - */ -class WalletConnectLinkIntentHandler : IntentHandler, AffectsNavigation { - - override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { - val intentData = intent?.data ?: return false - val scheme = intent.scheme ?: return false - - val wcUri = when (scheme) { - WC_SCHEME -> intentData.toString() - TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX) - else -> null - } - - return if (wcUri.isNullOrBlank()) { - false - } else { - val decodedWcUri = try { - URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME) - } catch (e: Exception) { - Timber.e(e) - return false - } - store.dispatchOnMain(WalletConnectAction.HandleDeepLink(decodedWcUri)) - true - } - } - - private companion object { - const val TANGEM_SCHEME = "tangem" - const val TANGEM_WC_PREFIX = "tangem://wc?uri=" - const val DEFAULT_CHARSET_NAME = "UTF-8" - const val WC_SCHEME = "wc" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt index c3567c8f3a..546cf5fd21 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt @@ -2,12 +2,15 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics -import com.tangem.tap.* -import com.tangem.tap.common.analytics.events.Onboarding.* -import com.tangem.tap.common.extensions.* +import com.tangem.tap.backupService +import com.tangem.tap.common.analytics.events.Onboarding.Finished +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -24,7 +27,7 @@ class BackupMiddleware { @Suppress("LongMethod", "ComplexMethod", "MagicNumber") private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { - if (DemoHelper.tryHandle(appState, action)) return + if (DemoHelper.tryHandle(appState)) return when (action) { is BackupAction.DiscardBackup -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index d2f1baf710..2aa7a3ef24 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -30,7 +30,7 @@ object TradeCryptoMiddleware { } private fun handle(state: () -> AppState?, action: TradeCryptoAction) { - if (DemoHelper.tryHandle(state, action)) return + if (DemoHelper.tryHandle(state)) return when (action) { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 8a842072d8..d107bc7422 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -20,7 +20,6 @@ import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -32,21 +31,19 @@ internal class WelcomeMiddleware { { action -> val appState = appStateProvider() if (action is WelcomeAction && appState != null) { - handleAction(action, appState.welcomeState) + handleAction(action) } next(action) } } } - private fun handleAction(action: WelcomeAction, state: WelcomeState) { + private fun handleAction(action: WelcomeAction) { mainScope.launch { when (action) { is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent) - is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( - afterUnlockIntent = action.afterUnlockIntent ?: state.intent, - ) - is WelcomeAction.ProceedWithCard -> proceedWithCard(afterScanIntent = state.intent) + is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics() + is WelcomeAction.ProceedWithCard -> proceedWithCard() is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() else -> Unit } @@ -70,13 +67,8 @@ internal class WelcomeMiddleware { } } - private suspend fun proceedWithBiometrics(afterUnlockIntent: Intent?) { - Timber.d( - """ - Proceeding with biometry - |- Intent: $afterUnlockIntent - """.trimIndent(), - ) + private suspend fun proceedWithBiometrics() { + Timber.d("Proceeding with biometry") val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.unlockIfLockable(type = UnlockType.ANY) @@ -93,20 +85,11 @@ internal class WelcomeMiddleware { store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(userWallet = selectedUserWallet) - - afterUnlockIntent?.let { - WalletConnectLinkIntentHandler().handleIntent(it, false) - } } } - private suspend fun proceedWithCard(afterScanIntent: Intent?) { - Timber.d( - """ - Proceeding with card - |- Intent: $afterScanIntent - """.trimIndent(), - ) + private suspend fun proceedWithCard() { + Timber.d("Proceeding with card") scanCardInternal { scanResponse -> val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse) @@ -125,10 +108,6 @@ internal class WelcomeMiddleware { store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) store.onUserWalletSelected(userWallet = userWallet) - - afterScanIntent?.let { - WalletConnectLinkIntentHandler().handleIntent(it, false) - } } } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 86116d6849..ef0fab7352 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -2,14 +2,12 @@ package com.tangem.tap.proxy.redux import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import org.rekotlin.Action sealed interface DaggerGraphAction : Action { data class SetActivityDependencies( val scanCardUseCase: ScanCardUseCase, - val walletConnectInteractor: WalletConnectInteractor, val cardSdkConfigRepository: CardSdkConfigRepository, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 76802dd20d..6a2e43b96a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -14,7 +14,6 @@ object DaggerGraphReducer { return when (action) { is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( scanCardUseCase = action.scanCardUseCase, - walletConnectInteractor = action.walletConnectInteractor, cardSdkConfigRepository = action.cardSdkConfigRepository, ) } 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 f7e6c83846..ef68891955 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 @@ -37,8 +37,6 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType @@ -46,8 +44,6 @@ data class DaggerGraphState( val networkConnectionManager: NetworkConnectionManager? = null, val cardScanningFeatureToggles: CardScanningFeatureToggles? = null, val scanCardUseCase: ScanCardUseCase? = null, - val walletConnectRepository: LegacyWalletConnectRepository? = null, - val walletConnectInteractor: WalletConnectInteractor? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, 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 653b45d4c1..29a693971b 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 @@ -8,22 +8,14 @@ import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent -import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.details.component.DetailsComponent 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 -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import com.tangem.features.hotwallet.WalletBackupComponent -import com.tangem.features.hotwallet.ViewPhraseComponent +import com.tangem.features.hotwallet.* import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -45,14 +37,12 @@ import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent -import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.features.tangempay.components.TangemPayOnboardingComponent @@ -84,7 +74,6 @@ internal class ChildFactory @Inject constructor( private val swapComponentFactory: SwapComponent.Factory, private val homeComponentFactory: HomeComponent.Factory, private val tokenDetailsComponentFactory: TokenDetailsComponent.Factory, - private val walletConnectComponentFactory: WalletConnectComponent.Factory, private val qrScanningComponentFactory: QrScanningComponent.Factory, private val accessCodeRecoveryComponentFactory: AccessCodeRecoveryComponent.Factory, private val cardSettingsComponentFactory: CardSettingsComponent.Factory, @@ -116,7 +105,6 @@ internal class ChildFactory @Inject constructor( private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, - private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -329,19 +317,11 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.WalletConnectSessions -> { - if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - createComponentChild( - context = context, - params = RedesignedWalletConnectComponent.Params(route.userWalletId), - componentFactory = redesignedWalletConnectComponentFactory, - ) - } else { - createComponentChild( - context = context, - params = WalletConnectComponent.Params(route.userWalletId), - componentFactory = walletConnectComponentFactory, - ) - } + createComponentChild( + context = context, + params = RedesignedWalletConnectComponent.Params(route.userWalletId), + componentFactory = redesignedWalletConnectComponentFactory, + ) } is AppRoute.QrScanning -> { val source = when (route.source) { 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 ec0d4d8507..b22c123693 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 @@ -19,10 +19,6 @@ "name": "STAKING_CARDANO_ENABLED", "version": "undefined" }, - { - "name": "WALLET_CONNECT_REDESIGN_ENABLED", - "version": "5.27.0" - }, { "name": "USEDESK_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 0fe3412528..a83cefd1ae 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 @@ -24,7 +24,6 @@ import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.WcRequestUseCaseFactory -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase @@ -88,7 +87,6 @@ internal object WalletConnectDataModule { fun defaultWcSessionsManager( store: WalletConnectStore, dispatchers: CoroutineDispatcherProvider, - legacyStore: WalletConnectSessionsRepository, getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, analytics: AnalyticsEventHandler, @@ -97,7 +95,6 @@ internal object WalletConnectDataModule { return DefaultWcSessionsManager( store = store, dispatchers = dispatchers, - legacyStore = legacyStore, getWallets = getWallets, wcNetworksConverter = wcNetworksConverter, analytics = analytics, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 69aa56551c..8697778af1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.sessions import arrow.core.Either import arrow.core.left import arrow.core.right -import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -16,7 +15,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO -import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -30,7 +28,6 @@ import kotlin.coroutines.resume @Suppress("LongParameterList") internal class DefaultWcSessionsManager( private val store: WalletConnectStore, - private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, @@ -39,18 +36,12 @@ internal class DefaultWcSessionsManager( ) : WcSessionsManager, WcSdkObserver { private val onSessionDelete = Channel(capacity = Channel.BUFFERED) - private val oneTimeMigration = MutableStateFlow(true) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } .transform { pair -> val (wallets, inStore) = pair val inSdk: List = WalletKit.getListOfActiveSessions() - if (oneTimeMigration.value) { - oneTimeMigration.value = false - val someMigrated = migrateLegacyStore(inStore, inSdk, wallets) - if (someMigrated) return@transform // ignore emit, wait next one - } val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one @@ -60,7 +51,6 @@ internal class DefaultWcSessionsManager( .flowOn(dispatchers.io) override fun onWcSdkInit() { - oneTimeMigration.value = true listenOnSessionDelete() extendSessions() } @@ -96,39 +86,6 @@ internal class DefaultWcSessionsManager( onSessionDelete.trySend(sessionDelete) } - private suspend fun migrateLegacyStore( - inNewStore: Set, - inSdk: List, - wallets: List, - ): Boolean { - val walletIds = wallets.map { wallet -> wallet.walletId } - val inLegacyStore = walletIds - .map { walletId -> - flow { - emit( - legacyStore.loadSessions(walletId.stringValue).mapNotNull { legacySession -> - val url = - inSdk.find { it.topic == legacySession.topic }?.metaData?.url ?: return@mapNotNull null - WcSessionDTO( - topic = legacySession.topic, - walletId = walletId, - url = url, - securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - ) - }, - ) - } - } - .merge() - .reduce { accumulator, value -> accumulator.plus(value) } - // migrate only active legacySessions - .filter { legacySession -> inSdk.any { inSdkSession -> inSdkSession.topic == legacySession.topic } } - - val mustSaveInNewStore = inLegacyStore.subtract(inNewStore) - if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore) - return mustSaveInNewStore.isNotEmpty() - } - private suspend fun associate( inSdk: List, inStore: Set, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt deleted file mode 100644 index 8c370b9fbd..0000000000 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.walletconnect.model.legacy - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Account( - @Json(name = "chainId") - val chainId: String, - - @Json(name = "walletAddress") - val walletAddress: String, - - @Json(name = "derivationPath") - val derivationPath: String?, -) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt deleted file mode 100644 index 5ca9f80ed4..0000000000 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.walletconnect.model.legacy - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Session( - @Json(name = "topic") - val topic: String, - - @Json(name = "accounts") - val accounts: List, -) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt deleted file mode 100644 index dce7f7f080..0000000000 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.domain.walletconnect.model.legacy - -interface WalletConnectSessionsRepository { - suspend fun loadSessions(userWallet: String): List - - suspend fun saveSession(userWallet: String, session: Session) - - suspend fun removeSession(userWallet: String, topic: String) -} \ No newline at end of file diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt deleted file mode 100644 index 3b9e35b56a..0000000000 --- a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.walletconnect.components - -interface WalletConnectFeatureToggles { - val isRedesignedWalletConnectEnabled: Boolean -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt index 5524f954e3..341ab307f2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt @@ -10,7 +10,6 @@ import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.model.WcEthMethodName import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.WcSolanaMethodName -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -21,18 +20,15 @@ internal class WcRoutingModel @Inject constructor( private val pairService: WcPairService, private val cardSdkProvider: CardSdkProvider, override val dispatchers: CoroutineDispatcherProvider, - private val featureToggles: WalletConnectFeatureToggles, ) : Model() { - val innerRouter = WcRouter(SlotNavigation()) + val innerRouter = WcRouter(SlotNavigation()) private val isSlotEmpty = MutableStateFlow(true) private val permittedAppRoute = MutableStateFlow(false) init { - if (featureToggles.isRedesignedWalletConnectEnabled) { - setupQueue() - } + setupQueue() } fun onSlotEmpty() { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt index 53c98210ad..5d82c115da 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt @@ -4,7 +4,6 @@ import android.net.Uri import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -15,7 +14,6 @@ import java.net.URLDecoder internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( @Assisted uri: Uri, getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - walletConnectFeatureToggles: WalletConnectFeatureToggles, wcPairService: WcPairService, ) : WalletConnectDeepLinkHandler { @@ -30,15 +28,13 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( }, ifRight = { wallet -> val decodedWcUri = URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME) - if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - wcPairService.pair( - request = WcPairRequest( - uri = decodedWcUri, - source = WcPairRequest.Source.DEEPLINK, - userWalletId = wallet.walletId, - ), - ) - } + wcPairService.pair( + request = WcPairRequest( + uri = decodedWcUri, + source = WcPairRequest.Source.DEEPLINK, + userWalletId = wallet.walletId, + ), + ) }, ) } catch (e: Exception) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 37e38fce69..559f845100 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -63,7 +63,6 @@ hilt-work = "1.0.0" hilt-compilerx = "1.0.0" jodatime = "2.12.1" kotlin-immutable-collections = "0.3.5" -kotsonGsonExt = "2.5.0" lottie = "3.4.0" lottie-compose = "6.6.0" moshi = "1.15.1" @@ -252,7 +251,6 @@ hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } hilt-compilerx = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-compilerx" } jodatime = { module = "joda-time:joda-time", version.ref = "jodatime" } -kotsonGson = { module = "com.github.salomonbrys.kotson:kotson", version.ref = "kotsonGsonExt" } lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie-compose" } material = { module = "com.google.android.material:material", version.ref = "googleMaterialComponent" } From 04bcb44f7619806e147ac0793b817a0ab96de821 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Sep 2025 19:12:30 +0400 Subject: [PATCH 58/69] Updated on 2026-08-14 --- .../api/common/response/ApiResponse.kt | 51 ++++++++++++------- .../response/ApiResponseCallDelegate.kt | 3 +- .../api/common/response/ApiResponseError.kt | 6 +-- .../api/common/response/ApiResponseExt.kt | 10 ++++ .../api/common/response/ApiResponseHeaders.kt | 3 ++ .../api/common/response/ResponseExt.kt | 7 +-- 6 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseHeaders.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt index 0348dba0e3..b85ea4e10e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt @@ -7,34 +7,51 @@ package com.tangem.datasource.api.common.response */ sealed class ApiResponse { - /** - * Represents a successful response from the API. - * - * @property data The data returned by the API. - */ - data class Success(val data: T) : ApiResponse() + /** Map of headers (header name with list of values) */ + abstract val headers: Map> /** - * Represents an error response or failure from the API. + * Represents a successful response from the API * - * @property cause The cause of the error. + * @property data the data returned by the API + * @property headers the headers returned by the API */ - data class Error(val cause: ApiResponseError) : ApiResponse() + data class Success( + val data: T, + override val headers: Map> = emptyMap(), + ) : ApiResponse() + + /** + * Represents an error response or failure from the API + * + * @property cause the cause of the error + * @property headers the headers returned by the API + */ + data class Error( + val cause: ApiResponseError, + override val headers: Map> = emptyMap(), + ) : ApiResponse() } /** - * Wraps data in a [ApiResponse.Success] instance. + * Wraps data in a [ApiResponse.Success] instance * - * @param data The data to wrap. - * @return A [ApiResponse.Success] instance containing the provided data. + * @param data the data to wrap + * @param headers the headers returned by the API + * @return a [ApiResponse.Success] instance containing the provided data */ -internal fun apiSuccess(data: T): ApiResponse = ApiResponse.Success(data) +internal fun apiSuccess(data: T, headers: Map>): ApiResponse { + return ApiResponse.Success(data, headers) +} /** - * Wraps an [ApiResponseError] in a [ApiResponse.Error] instance. + * Wraps an [ApiResponseError] in a [ApiResponse.Error] instance * - * @param cause The error to wrap. - * @return A [ApiResponse.Error] instance containing the provided error. + * @param cause the error to wrap + * @param headers the headers returned by the API + * @return a [ApiResponse.Error] instance containing the provided error */ @Suppress("UNCHECKED_CAST") -internal fun apiError(cause: ApiResponseError): ApiResponse = ApiResponse.Error(cause) as ApiResponse \ No newline at end of file +internal fun apiError(cause: ApiResponseError, headers: Map>): ApiResponse { + return ApiResponse.Error(cause, headers) as ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 50ee235c7b..28c7fa4f72 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -47,7 +47,8 @@ internal class ApiResponseCallDelegate( Timber.e(e, "onFailure UnknownException") ApiResponseError.UnknownException(e) } - val safeResponse = apiError(error) + + val safeResponse = apiError(cause = error, headers = emptyMap()) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index 10ec8add5d..f63826874b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -20,6 +20,8 @@ sealed class ApiResponseError : Exception() { // region Error Codes enum class Code(val numericCode: Int) { + // 3xx Server Errors + NOT_MODIFIED(numericCode = 304), // 4xx Server Errors BAD_REQUEST(numericCode = 400), UNAUTHORIZED(numericCode = 401), @@ -64,10 +66,6 @@ sealed class ApiResponseError : Exception() { ; override fun toString(): String = "$numericCode - $name" - - companion object { - val values = values() - } } // endregion Error Codes } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt index 0caae8ba1a..41a9f6bc8f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt @@ -19,4 +19,14 @@ inline fun catchApiResponseError(onError: (ApiResponseError) -> Unit, block: onError(e) throw e } +} + +/** + * Checks if the [ApiResponseError] is a network-related error. + * You can provide a custom predicate [codePredicate] to check for specific HTTP status codes. + */ +inline fun ApiResponseError.isNetworkError( + codePredicate: (ApiResponseError.HttpException.Code) -> Boolean = { true }, +): Boolean { + return this is ApiResponseError.HttpException && codePredicate(this.code) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseHeaders.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseHeaders.kt new file mode 100644 index 0000000000..52464501c9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseHeaders.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.api.common.response + +const val IF_NONE_MATCH_HEADER = "IfNoneMatch" \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index 926d9213aa..a8272de6f1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -12,12 +12,13 @@ import java.util.concurrent.TimeoutException import javax.net.ssl.SSLHandshakeException internal fun Response.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse { + val headers = headers().toMultimap() val body = body() return if (isSuccessful && body != null) { - apiSuccess(body) + apiSuccess(data = body, headers = headers) } else { - val code = ApiResponseError.HttpException.Code.values + val code = ApiResponseError.HttpException.Code.entries .firstOrNull { it.numericCode == code() } val e = try { if (code == null) { @@ -33,7 +34,7 @@ internal fun Response.toSafeApiResponse(analyticsErrorHandler: Anal ApiResponseError.UnknownException(e) } - apiError(e) + apiError(e, headers) } } From e5999953155e4b7d0360642bcccc6e4eda32585a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Sep 2025 14:33:06 +0500 Subject: [PATCH 59/69] Updated on 2026-08-14 --- .../com/tangem/tap/LockUserWalletsTimer.kt | 7 +++++ .../main/java/com/tangem/tap/MainActivity.kt | 5 ++++ .../tap/di/domain/WalletsDomainModule.kt | 20 +++++++++++++ .../wallets/hot/DefaultHotWalletAccessor.kt | 11 +++++++ .../domain/wallets/hot/HotWalletAccessor.kt | 4 +++ ...learAllHotWalletContextualUnlockUseCase.kt | 16 ++++++++++ .../GetHotWalletContextualUnlockUseCase.kt | 17 +++++++++++ .../accesscode/AccessCodeComponent.kt | 5 ++-- .../hotwallet/accesscode/AccessCodeModel.kt | 30 +++++++++++++------ .../entry/AddExistingWalletModel.kt | 4 +-- .../routing/AddExistingWalletChildFactory.kt | 2 -- .../updateaccesscode/UpdateAccessCodeModel.kt | 4 +-- .../routing/UpdateAccessCodeChildFactory.kt | 2 -- .../routing/UpdateAccessCodeRoute.kt | 9 ++++-- .../entry/WalletActivationModel.kt | 4 +-- .../routing/WalletActivationChildFactory.kt | 2 -- .../model/WalletSettingsModel.kt | 28 +++++++++++++++-- gradle/tangem_dependencies.toml | 2 +- 18 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearAllHotWalletContextualUnlockUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetHotWalletContextualUnlockUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index e3d0a18067..4a26816830 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -11,6 +11,7 @@ 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.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -22,6 +23,7 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import kotlin.time.Duration +@Suppress("LongParameterList") internal class LockUserWalletsTimer( private val context: Context, private val settingsRepository: SettingsRepository, @@ -30,6 +32,7 @@ internal class LockUserWalletsTimer( private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val coroutineScope: CoroutineScope, + private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase, ) : LifecycleOwner by context as LifecycleOwner, DefaultLifecycleObserver { @@ -55,6 +58,9 @@ internal class LockUserWalletsTimer( ) if (shouldOpenWelcomeScreenOnResume) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + clearAllHotWalletContextualUnlockUseCase.invoke() + } store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } @@ -120,6 +126,7 @@ internal class LockUserWalletsTimer( start() } .onRight { + clearAllHotWalletContextualUnlockUseCase.invoke() store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } } } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 7217bbef8d..287b4a7688 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -50,6 +50,7 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles @@ -186,6 +187,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + @Inject + internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase + @Inject internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles @@ -274,6 +278,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { coroutineScope = mainScope, userWalletsListRepository = userWalletsListRepository, hotWalletFeatureToggles = hotWalletFeatureToggles, + clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase, ) initIntentHandlers() 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 0f1f035a13..c116c9d81f 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 @@ -384,6 +384,16 @@ internal object WalletsDomainModule { ) } + @Provides + @Singleton + fun providesGetHotWalletContextualUnlockUseCase( + hotWalletAccessor: HotWalletAccessor, + ): GetHotWalletContextualUnlockUseCase { + return GetHotWalletContextualUnlockUseCase( + hotWalletAccessor = hotWalletAccessor, + ) + } + @Provides @Singleton fun providesClearHotWalletContextualUnlockUseCase( @@ -394,6 +404,16 @@ internal object WalletsDomainModule { ) } + @Provides + @Singleton + fun providesClearAllHotWalletContextualUnlockUseCase( + hotWalletAccessor: HotWalletAccessor, + ): ClearAllHotWalletContextualUnlockUseCase { + return ClearAllHotWalletContextualUnlockUseCase( + hotWalletAccessor = hotWalletAccessor, + ) + } + @Provides @Singleton fun providesExportSeedPhraseUseCase(hotWalletAccessor: HotWalletAccessor): ExportSeedPhraseUseCase { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index 70273f2684..33dc7635a7 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -52,6 +52,9 @@ class DefaultHotWalletAccessor @Inject constructor( } } + override fun getContextualUnlock(hotWalletId: HotWalletId): UnlockHotWallet? = + contextualUnlockHotWallet[hotWalletId] + override fun clearContextualUnlock(hotWalletId: HotWalletId) { contextualUnlockHotWallet[hotWalletId] = null scope.launch { @@ -59,6 +62,14 @@ class DefaultHotWalletAccessor @Inject constructor( } } + override fun clearAllContextualUnlock() { + val hotWalletsIds = contextualUnlockHotWallet.keys.toList() + contextualUnlockHotWallet.clear() + scope.launch { + hotWalletsIds.forEach { tangemHotSdk.clearUnlockContext(it) } + } + } + private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { val isAccessCodeRequired = walletsRepository.requireAccessCode() 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 index f0297ced18..41fc7f5fcb 100644 --- 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 @@ -12,5 +12,9 @@ interface HotWalletAccessor { suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet + fun getContextualUnlock(hotWalletId: HotWalletId): UnlockHotWallet? + fun clearContextualUnlock(hotWalletId: HotWalletId) + + fun clearAllContextualUnlock() } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearAllHotWalletContextualUnlockUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearAllHotWalletContextualUnlockUseCase.kt new file mode 100644 index 0000000000..0dddccd934 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ClearAllHotWalletContextualUnlockUseCase.kt @@ -0,0 +1,16 @@ + +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.hot.HotWalletAccessor + +class ClearAllHotWalletContextualUnlockUseCase( + private val hotWalletAccessor: HotWalletAccessor, +) { + + operator fun invoke(): Either { + return Either.catch { + hotWalletAccessor.clearAllContextualUnlock() + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetHotWalletContextualUnlockUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetHotWalletContextualUnlockUseCase.kt new file mode 100644 index 0000000000..629c4cad06 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetHotWalletContextualUnlockUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.hot.sdk.model.UnlockHotWallet + +class GetHotWalletContextualUnlockUseCase( + private val hotWalletAccessor: HotWalletAccessor, +) { + + suspend operator fun invoke(hotWalletId: HotWalletId): Either { + return Either.catch { + hotWalletAccessor.getContextualUnlock(hotWalletId) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index 5a044f85ef..160d1a228f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -34,12 +34,11 @@ internal class AccessCodeComponent @AssistedInject constructor( } interface ModelCallbacks { - fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) - fun onAccessCodeConfirmed(userWalletId: UserWalletId) + fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) + fun onAccessCodeUpdated(userWalletId: UserWalletId) } data class Params( - val isConfirmMode: Boolean, val accessCodeToConfirm: String? = null, val userWalletId: UserWalletId, val callbacks: ModelCallbacks, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index a3d0e3c9d7..e611297547 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -16,11 +16,14 @@ import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.SetAskBiometryShownUseCase import com.tangem.domain.settings.ShouldShowAskBiometryUseCase import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase +import com.tangem.domain.wallets.usecase.GetHotWalletContextualUnlockUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -38,6 +41,8 @@ internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getHotWalletContextualUnlockUseCase: GetHotWalletContextualUnlockUseCase, + private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase, private val userWalletsListRepository: UserWalletsListRepository, private val walletsRepository: WalletsRepository, private val tangemHotSdk: TangemHotSdk, @@ -49,13 +54,15 @@ internal class AccessCodeModel @Inject constructor( private val params = paramsContainer.require() + private var hotWalletId: HotWalletId? = null + internal val uiState: StateFlow field = MutableStateFlow(getInitialState()) private fun getInitialState() = AccessCodeUM( accessCode = "", onAccessCodeChange = ::onAccessCodeChange, - isConfirmMode = params.isConfirmMode, + isConfirmMode = params.accessCodeToConfirm != null, buttonEnabled = false, buttonInProgress = false, onButtonClick = ::onButtonClick, @@ -65,7 +72,7 @@ internal class AccessCodeModel @Inject constructor( uiState.update { it.copy( accessCode = value, - buttonEnabled = if (params.isConfirmMode) { + buttonEnabled = if (params.accessCodeToConfirm != null) { value == params.accessCodeToConfirm } else { value.length == uiState.value.accessCodeLength @@ -75,12 +82,10 @@ internal class AccessCodeModel @Inject constructor( } private fun onButtonClick() { - if (!params.isConfirmMode) { - params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode) + if (params.accessCodeToConfirm == null) { + params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode) } else { - params.accessCodeToConfirm?.let { - setCode(params.userWalletId, it) - } + setCode(params.userWalletId, params.accessCodeToConfirm) } } @@ -95,7 +100,9 @@ internal class AccessCodeModel @Inject constructor( .getOrElse { error("User wallet with id $userWalletId not found") } if (userWallet !is UserWallet.Hot) return@launch - val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId) + .getOrNull() + ?: UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) var updatedHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = unlockHotWallet, auth = HotAuth.Password(accessCode.toCharArray()), @@ -133,7 +140,7 @@ internal class AccessCodeModel @Inject constructor( ) } - params.callbacks.onAccessCodeConfirmed(params.userWalletId) + params.callbacks.onAccessCodeUpdated(params.userWalletId) }.onFailure { Timber.e(it) @@ -188,4 +195,9 @@ internal class AccessCodeModel @Inject constructor( return canUseBiometry && shouldShowAskBiometry } + + override fun onDestroy() { + hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) } + super.onDestroy() + } } \ No newline at end of file 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 c562032c4a..b218fa6aea 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 @@ -127,11 +127,11 @@ internal class AddExistingWalletModel @Inject constructor( } inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { - override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) { stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode)) } - override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index de7e7a76f2..6393ff0d6d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -46,7 +46,6 @@ internal class AddExistingWalletChildFactory @Inject constructor( is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = false, userWalletId = route.userWalletId, callbacks = model.accessCodeModelCallbacks, ), @@ -54,7 +53,6 @@ internal class AddExistingWalletChildFactory @Inject constructor( is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = true, accessCodeToConfirm = route.accessCode, userWalletId = route.userWalletId, callbacks = model.accessCodeModelCallbacks, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt index 19f8947793..5cf4e05649 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -35,11 +35,11 @@ internal class UpdateAccessCodeModel @Inject constructor( } } - override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) { stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode)) } - override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { router.pop() } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt index 354156a4c0..968560b8b3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt @@ -19,7 +19,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor( is UpdateAccessCodeRoute.SetAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = false, userWalletId = route.userWalletId, callbacks = model, ), @@ -27,7 +26,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor( is UpdateAccessCodeRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = true, accessCodeToConfirm = route.accessCode, userWalletId = route.userWalletId, callbacks = model, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt index a414995364..31d9113158 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt @@ -7,8 +7,13 @@ import kotlinx.serialization.Serializable internal sealed class UpdateAccessCodeRoute : Route { @Serializable - data class SetAccessCode(val userWalletId: UserWalletId) : UpdateAccessCodeRoute() + data class SetAccessCode( + val userWalletId: UserWalletId, + ) : UpdateAccessCodeRoute() @Serializable - data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : UpdateAccessCodeRoute() + data class ConfirmAccessCode( + val userWalletId: UserWalletId, + val accessCode: String, + ) : UpdateAccessCodeRoute() } \ No newline at end of file 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 722d2a7480..cfd50e4c72 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 @@ -141,11 +141,11 @@ internal class WalletActivationModel @Inject constructor( } inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { - override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) { stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) } - override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt index e6c13bdbd9..77684cecdb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt @@ -55,7 +55,6 @@ internal class WalletActivationChildFactory @Inject constructor( is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = false, userWalletId = model.params.userWalletId, callbacks = model.accessCodeModelCallbacks, ), @@ -63,7 +62,6 @@ internal class WalletActivationChildFactory @Inject constructor( is WalletActivationRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( context = childContext, params = AccessCodeComponent.Params( - isConfirmMode = true, accessCodeToConfirm = route.accessCode, userWalletId = model.params.userWalletId, callbacks = model.accessCodeModelCallbacks, 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 fac9ee92d6..3257456f21 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 @@ -46,6 +46,7 @@ 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.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -57,7 +58,6 @@ import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @ModelScoped internal class WalletSettingsModel @Inject constructor( - getWalletUseCase: GetUserWalletUseCase, paramsContainer: ParamsContainer, private val router: Router, private val messageSender: UiMessageSender, @@ -73,6 +73,7 @@ internal class WalletSettingsModel @Inject constructor( private val enableWalletNFTUseCase: EnableWalletNFTUseCase, private val disableWalletNFTUseCase: DisableWalletNFTUseCase, getWalletNotificationsEnabledUseCase: GetWalletNotificationsEnabledUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val settingsManager: SettingsManager, private val permissionsRepository: PermissionRepository, @@ -80,6 +81,7 @@ internal class WalletSettingsModel @Inject constructor( private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, + private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -144,7 +146,7 @@ internal class WalletSettingsModel @Inject constructor( ) } } - getWalletUseCase.invokeFlow(params.userWalletId) + getUserWalletUseCase.invokeFlow(params.userWalletId) .distinctUntilChanged() .filterIsInstance>() .flatMapLatest { combineUI(it.value) } @@ -361,7 +363,27 @@ internal class WalletSettingsModel @Inject constructor( if (!state.value.isWalletBackedUp) { messageSender.send(makeBackupAtFirstAlertBS) } else { - router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + val hotWalletId = userWallet.hotWalletId + when (hotWalletId.authType) { + HotWalletId.AuthType.NoPassword -> { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } + HotWalletId.AuthType.Password, + HotWalletId.AuthType.Biometry, + -> modelScope.launch { + unlockHotWalletContextualUseCase.invoke(hotWalletId) + .onLeft { + Timber.e("Unable to unlock wallet with id ${params.userWalletId}") + } + .onRight { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } + } + } + } } } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 80bdb6d116..4829355867 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ 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 ^ -tangemHotSdk = "develop-526" +tangemHotSdk = "develop-527" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From f39054b5a985ef28039d33eaa42a19b2a1d4e487 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 7 Sep 2025 12:44:07 +0400 Subject: [PATCH 60/69] Updated on 2026-08-14 --- .../api/common/response/ApiResponseExt.kt | 11 +- .../account/GetWalletAccountsResponse.kt | 2 +- .../FetchWalletAccountsErrorHandler.kt | 141 ++++++++ .../utils/GetWalletAccountsResponseExt.kt | 57 +++ .../FetchWalletAccountsErrorHandlerTest.kt | 239 +++++++++++++ .../utils/GetWalletAccountsResponseExtTest.kt | 324 ++++++++++++++++++ .../UserTokensResponseAccountIdEnricher.kt | 33 +- 7 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/utils/GetWalletAccountsResponseExt.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt index 41a9f6bc8f..a6b364259c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt @@ -21,12 +21,7 @@ inline fun catchApiResponseError(onError: (ApiResponseError) -> Unit, block: } } -/** - * Checks if the [ApiResponseError] is a network-related error. - * You can provide a custom predicate [codePredicate] to check for specific HTTP status codes. - */ -inline fun ApiResponseError.isNetworkError( - codePredicate: (ApiResponseError.HttpException.Code) -> Boolean = { true }, -): Boolean { - return this is ApiResponseError.HttpException && codePredicate(this.code) +/** Checks if the ApiResponseError is a network error with the specified HTTP status [code] */ +fun ApiResponseError.isNetworkError(code: ApiResponseError.HttpException.Code): Boolean { + return this is ApiResponseError.HttpException && this.code == code } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt index 6c3afd812f..ac0ce437eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -15,7 +15,7 @@ data class GetWalletAccountsResponse( @JsonClass(generateAdapter = true) data class Wallet( - @Json(name = "version") val version: Int, + @Json(name = "version") val version: Int = 0, @Json(name = "group") val group: GroupType, @Json(name = "sort") val sort: SortType, @Json(name = "totalAccounts") val totalAccounts: Int, diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt new file mode 100644 index 0000000000..7142520d72 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -0,0 +1,141 @@ +package com.tangem.data.account.fetcher + +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.utils.assignTokens +import com.tangem.data.account.utils.toUserTokensResponse +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code +import com.tangem.datasource.api.common.response.isNetworkError +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.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +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 com.tangem.utils.Provider +import timber.log.Timber +import javax.inject.Inject + +/** + * Handles errors that occur during the fetching of wallet accounts + * + * @property userTokensSaver saves user tokens to the storage + * @property userWalletsStore provides access to user wallet data + * @property userTokensResponseStore provides access to user token responses. + * @property cryptoPortfolioCF factory for converting crypto portfolios + * @property userTokensResponseFactory factory for creating user token responses + * @property cardCryptoCurrencyFactory factory for creating default cryptocurrencies for multi-currency wallets + * + * @see DefaultWalletAccountsFetcher + * +[REDACTED_AUTHOR] + */ +internal class FetchWalletAccountsErrorHandler @Inject constructor( + private val userTokensSaver: UserTokensSaver, + private val userWalletsStore: UserWalletsStore, + private val userTokensResponseStore: UserTokensResponseStore, + private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, + private val userTokensResponseFactory: UserTokensResponseFactory, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, +) { + + /** + * Handles the error that occurred during the fetching of wallet accounts. + * [pushWalletAccounts] and [storeWalletAccounts] are functions that passed as parameters to avoid + * cyclic dependencies. + * + * @param error the error that occurred + * @param userWalletId the ID of the user wallet + * @param savedAccountsResponse the previously saved wallet accounts response, if available + * @param pushWalletAccounts function to push wallet accounts to the server + * @param storeWalletAccounts function to store wallet accounts locally + */ + suspend fun handle( + error: ApiResponseError, + userWalletId: UserWalletId, + savedAccountsResponse: GetWalletAccountsResponse?, + pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List) -> Unit, + storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit, + ) { + val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) + if (isResponseUpToDate) { + Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") + return + } + + val userWalletProvider = Provider { userWalletsStore.getSyncStrict(key = userWalletId) } + val accountDTOs = savedAccountsResponse?.accounts.orDefault(userWalletProvider = userWalletProvider) + + val userTokensResponse = savedAccountsResponse?.toUserTokensResponse() + .orFromLegacyStore(userWalletProvider = userWalletProvider) + .orDefault(userWalletProvider = userWalletProvider) + + val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND) + if (isNotFoundError) { + pushWalletAccounts(userWalletId, accountDTOs) + userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse) + } + + val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse) + storeWalletAccounts(userWalletId, response) + } + + private fun List?.orDefault(userWalletProvider: Provider): List { + if (this != null) return this + + val userWallet = userWalletProvider() + + val accounts = AccountList.empty(userWallet).accounts + .filterIsInstance() + + val converter = cryptoPortfolioCF.create(userWallet = userWallet) + + return converter.convertListBack(input = accounts) + } + + private suspend fun UserTokensResponse?.orFromLegacyStore( + userWalletProvider: Provider, + ): UserTokensResponse? { + if (this != null) return this + + return userTokensResponseStore.getSyncOrNull( + userWalletId = userWalletProvider().walletId, + ) + } + + private fun UserTokensResponse?.orDefault(userWalletProvider: Provider): UserTokensResponse { + if (this != null) return this + + return userTokensResponseFactory.createUserTokensResponse( + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( + userWallet = userWalletProvider(), + ), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + + private fun GetWalletAccountsResponse?.orDefault( + userWalletId: UserWalletId, + accountDTOs: List, + userTokensResponse: UserTokensResponse, + ): GetWalletAccountsResponse { + if (this != null) return this + + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = userTokensResponse.group, + sort = userTokensResponse.sort, + totalAccounts = accountDTOs.size, + ), + accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens), + unassignedTokens = emptyList(), + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/GetWalletAccountsResponseExt.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/GetWalletAccountsResponseExt.kt new file mode 100644 index 0000000000..caace442d6 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/GetWalletAccountsResponseExt.kt @@ -0,0 +1,57 @@ +package com.tangem.data.account.utils + +import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher +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.models.wallet.UserWalletId + +/** Flattens the tokens from all wallet accounts into a single list */ +internal fun GetWalletAccountsResponse.flattenTokens(): List { + return accounts.flatMap { it.tokens.orEmpty() } +} + +/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */ +internal fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse { + return UserTokensResponse( + group = wallet.group, + sort = wallet.sort, + tokens = flattenTokens(), + ) +} + +/** + * Assigns tokens from a [UserTokensResponse] to the wallet accounts in the [GetWalletAccountsResponse] + * + * @param userWalletId the ID of the user wallet + * + * @return a new [GetWalletAccountsResponse]` with tokens assigned to the wallet accounts + */ +internal fun GetWalletAccountsResponse.assignTokens(userWalletId: UserWalletId): GetWalletAccountsResponse { + return copy( + accounts = accounts.assignTokens(userWalletId = userWalletId, tokens = unassignedTokens), + unassignedTokens = emptyList(), + ) +} + +/** + * Assigns tokens from a [UserTokensResponse] to a list of wallet accounts + * + * @param userWalletId the ID of the user wallet + * @param tokens tokens to be assigned + * + * @return a new list of [WalletAccountDTO] with tokens assigned to each account + */ +internal fun List.assignTokens( + userWalletId: UserWalletId, + tokens: List, +): List { + val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens) + .groupBy { it.accountId } + + return map { accountDTO -> + accountDTO.copy( + tokens = enrichedTokens[accountDTO.id].orEmpty(), + ) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt new file mode 100644 index 0000000000..5d41955371 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -0,0 +1,239 @@ +package com.tangem.data.account.fetcher + +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.utils.toUserTokensResponse +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code +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.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +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.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class FetchWalletAccountsErrorHandlerTest { + + private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) + private val userWalletsStore: UserWalletsStore = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk() + private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk() + private val cryptoPortfolioConverter = mockk() + private val userTokensResponseFactory: UserTokensResponseFactory = mockk() + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + + private val handler = FetchWalletAccountsErrorHandler( + userTokensSaver = userTokensSaver, + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + cryptoPortfolioCF = cryptoPortfolioCF, + userTokensResponseFactory = userTokensResponseFactory, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + ) + + private val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + } + + @BeforeEach + fun setupEach() { + clearMocks( + userTokensSaver, + userWalletsStore, + userTokensResponseStore, + cryptoPortfolioCF, + cryptoPortfolioConverter, + cardCryptoCurrencyFactory, + ) + } + + @Test + fun `does not update accounts when response is up to date`() = runTest { + // Arrange + val error = ApiResponseError.HttpException( + code = Code.NOT_MODIFIED, + message = "Not Modified", + errorBody = null, + ) + + val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk() + val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk() + + // Act + handler.handle( + error = error, + userWalletId = userWalletId, + savedAccountsResponse = null, + pushWalletAccounts = pushWalletAccounts, + storeWalletAccounts = storeWalletAccounts, + ) + + // Assert + coVerify(inverse = true) { + userWalletsStore.getSyncStrict(key = any()) + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) + cryptoPortfolioCF.create(any()) + cryptoPortfolioConverter.convertListBack(any()) + pushWalletAccounts(any(), any()) + userTokensSaver.push(userWalletId = any(), response = any()) + storeWalletAccounts(any(), any()) + } + } + + @Test + fun `pushes and stores accounts when NOT_FOUND error occurs`() = runTest { + // Arrange + val error = ApiResponseError.HttpException( + code = Code.NOT_FOUND, + message = "Not Found", + errorBody = null, + ) + + val accountDTO = WalletAccountDTO( + id = "nibh", + name = "Michael Dotson", + derivationIndex = 7135, + icon = "consectetuer", + iconColor = "ferri", + tokens = listOf(), + totalTokens = 7738, + totalNetworks = 3348, + ) + + val savedAccountsResponse = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 1, + ), + accounts = listOf(accountDTO), + unassignedTokens = emptyList(), + ) + + val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) + val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) + + // Act + handler.handle( + error = error, + userWalletId = userWalletId, + savedAccountsResponse = savedAccountsResponse, + pushWalletAccounts = pushWalletAccounts, + storeWalletAccounts = storeWalletAccounts, + ) + + // Assert + coVerify { + pushWalletAccounts(userWalletId, listOf(accountDTO)) + userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse()) + storeWalletAccounts(userWalletId, savedAccountsResponse) + } + + coVerify(inverse = true) { + userWalletsStore.getSyncStrict(key = any()) + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) + cryptoPortfolioCF.create(any()) + cryptoPortfolioConverter.convertListBack(any()) + } + } + + @Test + fun `uses default accounts when savedAccountsResponse is null`() = runTest { + // Arrange + val error = ApiResponseError.TimeoutException + + val accounts = AccountList.empty(userWallet).accounts + .filterIsInstance() + + val accountDTO = WalletAccountDTO( + id = "nibh", + name = "Michael Dotson", + derivationIndex = 7135, + icon = "consectetuer", + iconColor = "ferri", + tokens = listOf(), + totalTokens = 7738, + totalNetworks = 3348, + ) + + val savedAccountsResponse = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 1, + ), + accounts = listOf(accountDTO), + unassignedTokens = emptyList(), + ) + + val userTokensResponse = savedAccountsResponse.toUserTokensResponse() + + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter + every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO) + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns userTokensResponse + every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList() + + val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) + val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) + + // Act + handler.handle( + error = error, + userWalletId = userWalletId, + savedAccountsResponse = null, + pushWalletAccounts = pushWalletAccounts, + storeWalletAccounts = storeWalletAccounts, + ) + + // Assert + coVerify { + userWalletsStore.getSyncStrict(userWalletId) + cryptoPortfolioCF.create(userWallet) + cryptoPortfolioConverter.convertListBack(accounts) + userTokensResponseStore.getSyncOrNull(userWalletId) + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) + storeWalletAccounts(userWalletId, any()) + } + + coVerify(inverse = true) { + pushWalletAccounts(any(), any()) + userTokensSaver.push(userWalletId = any(), response = any()) + } + } + + private companion object { + + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt new file mode 100644 index 0000000000..8f9089305d --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt @@ -0,0 +1,324 @@ +package com.tangem.data.account.utils + +import com.google.common.truth.Truth +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.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetWalletAccountsResponseExtTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class FlattenTokens { + + @Test + fun `flattenTokens returns empty list when accounts are empty`() { + // Arrange + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + + // Act + val actual = response.flattenTokens() + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `flattenTokens returns empty list when single account has empty tokens`() { + // Arrange + val account = createWalletAccountDTO(derivationIndex = 0) + + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 1, + ), + accounts = listOf(account), + unassignedTokens = emptyList(), + ) + + // Act + val actual = response.flattenTokens() + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `flattenTokens returns all tokens from multiple accounts`() { + // Arrange + val token1 = createUserToken(id = "0") + val token2 = createUserToken(id = "1") + val token3 = createUserToken(id = "2") + + val account1 = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1, token2)) + val account2 = createWalletAccountDTO(derivationIndex = 1, tokens = listOf(token3)) + val account3 = createWalletAccountDTO(derivationIndex = 2) + + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 2, + ), + accounts = listOf(account1, account2, account3), + unassignedTokens = emptyList(), + ) + + // Act + val actual = response.flattenTokens() + + // Assert + Truth.assertThat(actual).containsExactly(token1, token2, token3) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ToUserTokensResponse { + + @Test + fun `toUserTokensResponse returns correct UserTokensResponse for empty accounts and unassignedTokens`() { + // Arrange + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + + // Act + val actual = response.toUserTokensResponse() + + // Assert + val expected = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = emptyList(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `toUserTokensResponse includes tokens from accounts and unassignedTokens`() { + // Arrange + val token1 = createUserToken(id = "0") + val token2 = createUserToken(id = "1") + val account = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1)) + val unassignedToken = token2 + + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 1, + ), + accounts = listOf(account), + unassignedTokens = listOf(unassignedToken), + ) + + // Act + val actual = response.toUserTokensResponse() + + // Assert + val expected = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = listOf(token1), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetWalletAccountsResponseAssignTokens { + + @Test + fun `assignTokens correctly assigns tokens to accounts`() { + // Arrange + val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027" + val token1 = createUserToken(id = "0", accountId = null) + val token2 = createUserToken(id = "1", accountId = null) + val account1 = createWalletAccountDTO(derivationIndex = 0) + val account2 = createWalletAccountDTO(derivationIndex = 1) + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 2, + ), + accounts = listOf(account1, account2), + unassignedTokens = listOf(token1, token2), + ) + + // Act + val actual = response.assignTokens(userWalletId) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = response.wallet, + accounts = listOf( + account1.copy( + tokens = listOf( + token1.copy(accountId = accountId), + token2.copy(accountId = accountId), + ), + ), + account2, + ), + unassignedTokens = emptyList(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `assignTokens does not change accounts if there are no unassignedTokens`() { + // Arrange + val account = createWalletAccountDTO(derivationIndex = 0) + val response = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 1, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + totalAccounts = 1, + ), + accounts = listOf(account), + unassignedTokens = emptyList(), + ) + + // Act + val actual = response.assignTokens(userWalletId) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = response.wallet, + accounts = listOf(account), + unassignedTokens = emptyList(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class WalletAccountDTOListAssignTokens { + + @Test + fun `assignTokens correctly assigns tokens to accounts`() { + // Arrange + val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027" + val token1 = createUserToken(id = "0", accountId = null) + val token2 = createUserToken(id = "1", accountId = null) + val account1 = createWalletAccountDTO(derivationIndex = 0) + val account2 = createWalletAccountDTO(derivationIndex = 1) + + // Act + val actual = listOf(account1, account2).assignTokens( + userWalletId = userWalletId, + tokens = listOf(token1, token2), + ) + + // Assert + val expected = listOf( + account1.copy( + tokens = listOf( + token1.copy(accountId = accountId), + token2.copy(accountId = accountId), + ), + ), + account2, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `assignTokens does not change accounts if there are no unassignedTokens`() { + // Arrange + val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027" + val token1 = createUserToken(id = "0", accountId = accountId) + val account1 = createWalletAccountDTO(derivationIndex = 0) + + // Act + val actual = listOf(account1).assignTokens( + userWalletId = userWalletId, + tokens = listOf(token1), + ) + + // Assert + val expected = listOf( + account1.copy( + tokens = listOf( + token1.copy(accountId = accountId), + ), + ), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + private fun createWalletAccountDTO(derivationIndex: Int, tokens: List = emptyList()) = + WalletAccountDTO( + id = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!, + ).value, + name = "Name #$derivationIndex", + derivationIndex = derivationIndex, + icon = "icon", + iconColor = "color", + tokens = tokens, + totalTokens = tokens.size, + totalNetworks = 1, + ) + + private fun createUserToken(id: String, accountId: String? = "account_id") = UserTokensResponse.Token( + id = id, + accountId = accountId, + networkId = "ethereum", + derivationPath = "m/44'/60'/0'/0/0", + name = "Token", + symbol = "T", + contractAddress = "0x$id", + decimals = 18, + ) + + private companion object { + + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt index b52d3c3f86..f91e9f40f3 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt @@ -24,23 +24,36 @@ object UserTokensResponseAccountIdEnricher { * @param response the [UserTokensResponse] containing tokens to be enriched */ operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse { - val hasUnassignedTokens = response.tokens.any { it.accountId == null } - if (!hasUnassignedTokens) return response + val enrichedTokens = invoke(userWalletId = userWalletId, tokens = response.tokens) - val enrichedTokens = response.tokens + return response.copy(tokens = enrichedTokens) + } + + /** + * Enriches the given list of tokens with accountId values + * + * @param userWalletId the ID of the user wallet + * @param tokens the list of tokens to be enriched + */ + operator fun invoke( + userWalletId: UserWalletId, + tokens: List, + ): List { + val hasUnassignedTokens = tokens.any { it.accountId == null } + if (!hasUnassignedTokens) return tokens + + val enrichedTokens = tokens .filter { it.accountId == null } .groupByAccountIndex() .mapKeysToAccountId(userWalletId) .mapToEnrichedTokens() - if (enrichedTokens.isEmpty()) return response + if (enrichedTokens.isEmpty()) return tokens - return response.copy( - tokens = response.tokens.map { token -> - val enrichedToken = enrichedTokens.find { it == token } - enrichedToken ?: token - }, - ) + return tokens.map { token -> + val enrichedToken = enrichedTokens.firstOrNull { it == token } + enrichedToken ?: token + } } private fun List.groupByAccountIndex(): Map> { From 551e2a60bbbf61569fdf4e1ceeccfb250a3d4893 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 10:04:56 +0200 Subject: [PATCH 61/69] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 2 + core/res/src/main/res/values/strings.xml | 1 + .../src/main/res/drawable/ic_best_rate_12.xml | 16 + .../main/res/drawable/ic_staking_mini_10.xml | 9 + .../onramp/GetOnrampAllOffersUseCase.kt | 6 +- .../domain/onramp/GetOnrampOffersUseCase.kt | 48 +-- .../domain/onramp/utils/OnrampOfferUtils.kt | 26 ++ .../onramp/GetOnrampOffersUseCaseTest.kt | 8 + .../onramp/alloffers/AllOffersComponent.kt | 3 + .../alloffers/DefaultAllOffersComponent.kt | 9 +- .../alloffers/entity/AllOffersIntents.kt | 14 + .../alloffers/entity/AllOffersStateFactory.kt | 154 +++++++++ .../alloffers/entity/AllOffersStateUM.kt | 34 ++ .../onramp/alloffers/model/AllOffersModel.kt | 80 ++++- .../alloffers/ui/AllOffersContentSheet.kt | 322 ++++++++++++++++++ .../alloffers/ui/PaymentMethodsContent.kt | 262 ++++++++++++++ .../mainv2/DefaultOnrampV2MainComponent.kt | 4 +- .../di/OnrampMainV2ComponentModelModule.kt | 2 +- .../entity/OnrampV2MainBottomSheetConfig.kt | 2 +- .../OnrampV2AmountFieldChangeConverter.kt | 1 + .../model/OnrampV2MainComponentModel.kt | 6 +- .../onramp/mainv2/ui/OnrampOffersContent.kt | 13 +- 22 files changed, 985 insertions(+), 37 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_best_rate_12.xml create mode 100644 core/ui/src/main/res/drawable/ic_staking_mini_10.xml create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt 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 18f3540843..a0e87e1ab1 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 @@ -254,11 +254,13 @@ internal object OnrampDomainModule { onrampRepository: OnrampRepository, onrampErrorResolver: OnrampErrorResolver, onrampTransactionRepository: OnrampTransactionRepository, + settingsRepository: SettingsRepository, ): GetOnrampOffersUseCase { return GetOnrampOffersUseCase( onrampRepository = onrampRepository, errorResolver = onrampErrorResolver, onrampTransactionRepository = onrampTransactionRepository, + settingsRepository = settingsRepository, ) } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b475fe441f..c3d240ace5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -898,6 +898,7 @@ %s min You get You can close this screen and check the transaction status on the token details screen. + Up to Via You will pay Group diff --git a/core/ui/src/main/res/drawable/ic_best_rate_12.xml b/core/ui/src/main/res/drawable/ic_best_rate_12.xml new file mode 100644 index 0000000000..99e0e500c2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_best_rate_12.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_staking_mini_10.xml b/core/ui/src/main/res/drawable/ic_staking_mini_10.xml new file mode 100644 index 0000000000..69aab19b6c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_staking_mini_10.xml @@ -0,0 +1,9 @@ + + + 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 index f2f787e182..a7e1d2754f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt @@ -13,6 +13,7 @@ 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.utils.calculateRateDif +import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -38,7 +39,7 @@ class GetOnrampAllOffersUseCase( if (validQuotes.isEmpty()) return emptyList() val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() - val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val bestRate = overallBestRateQuote?.toAmount?.value val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } @@ -54,7 +55,8 @@ class GetOnrampAllOffersUseCase( OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages) } - val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } + val groupBestRateOfferData = + methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val groupBestRateOffer = methodOffers.find { when (val quote = it.quote) { is OnrampQuote.Data -> quote == groupBestRateOfferData 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 index 1632abd3cc..81ee5e82fd 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -12,15 +12,17 @@ import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif +import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.settings.repositories.SettingsRepository 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, + private val settingsRepository: SettingsRepository, ) { operator fun invoke( @@ -37,14 +39,15 @@ class GetOnrampOffersUseCase( .catch { throwable -> errorResolver.resolve(throwable).left() } } - private fun processOffers( + private suspend fun processOffers( quotes: List, transactions: List, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() - val bestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() + val bestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val bestRate = bestRateQuote?.toAmount?.value val offers = validQuotes.map { quote -> @@ -53,8 +56,8 @@ class GetOnrampOffersUseCase( } val recentOffer = findRecentOffer(offers, transactions) - val bestRateOffer = findBestRateOffer(offers) - val fastestOffer = findFastestOffer(offers) + val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) return buildOffersBlocks( recentOffer = recentOffer, @@ -73,36 +76,35 @@ class GetOnrampOffersUseCase( } } - 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 findBestRateOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { + return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List): OnrampOffer? { + private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): 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 - } - } + instantOffers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } 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 + fastestOffers.maxWithOrNull(offerComparator(isGooglePayAvailable)) + } + } + + private fun offerComparator(isGooglePayAvailable: Boolean): Comparator = Comparator { offer1, offer2 -> + when (val quote1 = offer1.quote) { + is OnrampQuote.Data -> { + when (val quote2 = offer2.quote) { + is OnrampQuote.Data -> { + compareOffersByRateSpeedAndPriority(isGooglePayAvailable).compare(quote1, quote2) + } + else -> 1 } } + else -> -1 } } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt index 0b3625d494..7329a88a84 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt @@ -1,8 +1,34 @@ package com.tangem.domain.onramp.utils +import com.tangem.domain.onramp.model.OnrampQuote import java.math.BigDecimal internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? { if (bestRate == null) return null return BigDecimal.ONE - currentTokenRate / bestRate +} + +internal fun compareOffersByRateSpeedAndPriority(isGooglePayAvailable: Boolean): Comparator { + return Comparator { quote1, quote2 -> + val rateComparison = quote1 + .toAmount + .value + .compareTo(quote2.toAmount.value) + if (rateComparison != 0) return@Comparator rateComparison + + val speedComparison = + quote2 + .paymentMethod + .type + .getProcessingSpeed() + .speed + .compareTo(quote1.paymentMethod.type.getProcessingSpeed().speed) + if (speedComparison != 0) return@Comparator speedComparison + + quote1 + .paymentMethod + .type + .getPriority(isGooglePayAvailable) + .compareTo(quote2.paymentMethod.type.getPriority(isGooglePayAvailable)) + } } \ 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 index 62b55b230a..87f9fbb1c6 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -8,6 +8,7 @@ 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 com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -22,6 +23,7 @@ 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 settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) private val userWalletId: UserWalletId = mockk(relaxUnitFun = true) @@ -34,6 +36,7 @@ class GetOnrampOffersUseCaseTest { onrampRepository = onrampRepository, onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, + settingsRepository = settingsRepository, ) } @@ -42,6 +45,7 @@ class GetOnrampOffersUseCaseTest { val emptyQuotes = listOf() val emptyTransactions = listOf() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( emptyTransactions, @@ -77,6 +81,7 @@ class GetOnrampOffersUseCaseTest { createMockTransaction("provider1", "card", 1000L), ) + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -121,6 +126,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -165,6 +171,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -218,6 +225,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt index 7c2de6da52..d2d2615c0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.onramp.model.OnrampProviderWithQuote internal interface AllOffersComponent : ComposableBottomSheetComponent { @@ -11,6 +12,8 @@ internal interface AllOffersComponent : ComposableBottomSheetComponent { val userWallet: UserWallet, val cryptoCurrency: CryptoCurrency, val onDismiss: () -> Unit, + val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, + val amountCurrencyCode: String, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt index 02adaaa504..12c4ec31f9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt @@ -1,9 +1,12 @@ package com.tangem.features.onramp.alloffers import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.onramp.alloffers.model.AllOffersModel +import com.tangem.features.onramp.alloffers.ui.AllOffersContentSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,7 +24,11 @@ internal class DefaultAllOffersComponent @AssistedInject constructor( @Composable override fun BottomSheet() { - // TODO to be continued in [REDACTED_TASK_KEY] + val state by model.state.collectAsState() + AllOffersContentSheet( + state = state, + onCloseClick = { dismiss() }, + ) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt new file mode 100644 index 0000000000..4d2bbcec90 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.alloffers.entity + +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + +interface AllOffersIntents { + + fun onPaymentMethodClicked(paymentMethodId: String) + + fun onBuyClick(quote: OnrampProviderWithQuote.Data) + + fun onBackClicked() + + fun onRefresh() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt new file mode 100644 index 0000000000..67878d0738 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -0,0 +1,154 @@ +package com.tangem.features.onramp.alloffers.entity + +import com.tangem.common.ui.notifications.NotificationUM +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.format.bigdecimal.percent +import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.MINUS +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class AllOffersStateFactory( + private val currentStateProvider: Provider, + private val allOffersIntents: AllOffersIntents, +) { + + fun getLoadedPaymentsState(methodGroups: List): AllOffersStateUM { + return AllOffersStateUM.Content( + methods = methodGroups.map { methodGroup -> + AllOffersPaymentMethodUM( + offers = mapOffersToUM(methodGroup.offers).toPersistentList(), + methodConfig = OnrampPaymentMethodConfig( + method = methodGroup.paymentMethod, + onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) }, + ), + diff = methodGroup + .bestRateOffer + ?.rateDif + ?.takeIf { it > BigDecimal.ZERO } + ?.let { diff -> + stringReference("$MINUS${diff.format { percent() }}") + }, + rate = methodGroup.bestRateOffer?.let { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value.format { + crypto( + symbol = quote.toAmount.symbol, + decimals = quote.toAmount.decimals, + ) + } + else -> "" + } + } ?: "", + providersCount = methodGroup.providerCount, + isBestRate = methodGroup.isBestPaymentMethod, + ) + }.toPersistentList(), + currentMethod = null, + onBackClicked = { allOffersIntents.onBackClicked() }, + ) + } + + fun getPaymentsState(): AllOffersStateUM { + return when (val currentState = currentStateProvider.invoke()) { + is AllOffersStateUM.Content -> { + currentState.copy(currentMethod = null) + } + AllOffersStateUM.Loading, + is AllOffersStateUM.Error, + -> currentState + } + } + + fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM { + return when (onrampError) { + is OnrampError.DataError -> getErrorState( + errorCode = onrampError.code, + onRefresh = allOffersIntents::onRefresh, + ) + OnrampError.PairsNotFound, + is OnrampError.DomainError, + -> getErrorState(onRefresh = allOffersIntents::onRefresh) + is OnrampError.AmountError.TooBigError, + is OnrampError.AmountError.TooSmallError, + OnrampError.RedirectError.VerificationFailed, + OnrampError.RedirectError.WrongRequestId, + -> currentStateProvider() + } + } + + private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM { + val state = currentStateProvider() + return when (state) { + is AllOffersStateUM.Content, + AllOffersStateUM.Loading, + -> AllOffersStateUM.Error( + errorNotification = NotificationUM.Warning.OnrampErrorNotification( + errorCode = errorCode, + onRefresh = onRefresh, + ), + ) + is AllOffersStateUM.Error -> state + } + } + + private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM { + return when (advantages) { + OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default + OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate + OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest + } + } + + private fun mapOffersToUM(offers: List): List { + return buildList { + offers.forEach { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> { + add( + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = mapOfferAdvantagesDTOtoUM(offer.advantages), + paymentMethod = quote.paymentMethod, + providerId = quote.provider.id, + providerName = quote.provider.info.name, + rate = quote.toAmount.value.format { + crypto( + symbol = quote.toAmount.symbol, + decimals = quote.toAmount.decimals, + ) + }, + diff = offer + .rateDif + ?.takeIf { it > BigDecimal.ZERO } + ?.let { diff -> + stringReference("$MINUS${diff.format { percent() }}") + }, + onBuyClicked = { + allOffersIntents.onBuyClick( + OnrampProviderWithQuote.Data( + provider = quote.provider, + paymentMethod = quote.paymentMethod, + toAmount = quote.toAmount, + fromAmount = quote.fromAmount, + ), + ) + }, + ), + ) + } + is OnrampQuote.AmountError, + is OnrampQuote.Error, + -> Unit + } + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt new file mode 100644 index 0000000000..834bc396b3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.alloffers.entity + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface AllOffersStateUM { + + data object Loading : AllOffersStateUM + + data class Content( + val methods: ImmutableList, + val currentMethod: AllOffersPaymentMethodUM? = null, + val onBackClicked: () -> Unit, + ) : AllOffersStateUM + + data class Error(val errorNotification: NotificationUM) : AllOffersStateUM +} + +internal data class AllOffersPaymentMethodUM( + val offers: ImmutableList, + val methodConfig: OnrampPaymentMethodConfig, + val diff: TextReference?, + val rate: String, + val providersCount: Int, + val isBestRate: Boolean, +) + +internal data class OnrampPaymentMethodConfig( + val method: OnrampPaymentMethod, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 3280c17cb9..19e69d6696 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -1,19 +1,97 @@ package com.tangem.features.onramp.alloffers.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.onramp.GetOnrampAllOffersUseCase +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.features.onramp.alloffers.AllOffersComponent +import com.tangem.features.onramp.alloffers.entity.AllOffersIntents +import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory +import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject internal class AllOffersModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val getOnrampAllOffersUseCase: GetOnrampAllOffersUseCase, paramsContainer: ParamsContainer, -) : Model() { +) : Model(), AllOffersIntents { + + private var quotesJob: Job? = null + + private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { + AllOffersStateFactory( + currentStateProvider = Provider { state.value }, + allOffersIntents = this, + ) + } private val params: AllOffersComponent.Params = paramsContainer.require() + private val _state: MutableStateFlow = MutableStateFlow(AllOffersStateUM.Loading) + val state: StateFlow = _state.asStateFlow() + + init { + subscribeOnAllOffers() + } + fun dismiss() { params.onDismiss() } + + override fun onPaymentMethodClicked(paymentMethodId: String) { + val contentState = state.value as? AllOffersStateUM.Content ?: return + val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return + _state.update { contentState.copy(currentMethod = method) } + } + + override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + analyticsEventHandler.send( + OnrampAnalyticsEvent.OnBuyClick( + providerName = quote.provider.info.name, + currency = params.amountCurrencyCode, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + params.openRedirectPage(quote) + } + + override fun onBackClicked() { + _state.update { stateFactory.getPaymentsState() } + } + + override fun onRefresh() { + subscribeOnAllOffers() + } + + private fun subscribeOnAllOffers() { + quotesJob?.cancel() + quotesJob = modelScope.launch(dispatchers.default) { + getOnrampAllOffersUseCase.invoke( + userWalletId = params.userWallet.walletId, + cryptoCurrencyId = params.cryptoCurrency.id, + ).collectLatest { maybeOffers -> + maybeOffers.fold( + ifLeft = ::handleOnrampError, + ifRight = { offersGroup -> + _state.update { stateFactory.getLoadedPaymentsState(offersGroup) } + }, + ) + } + } + } + + private fun handleOnrampError(onrampError: OnrampError) { + Timber.e(onrampError.toString()) + _state.update { stateFactory.getOnrampErrorState(onrampError) } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt new file mode 100644 index 0000000000..f60660546a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -0,0 +1,322 @@ +package com.tangem.features.onramp.alloffers.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodType +import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM +import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM +import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.mainv2.ui.Offer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) { + val onBack = remember(state) { + { + if (state is AllOffersStateUM.Content && state.currentMethod != null) { + state.onBackClicked() + } else { + onCloseClick() + } + } + } + + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBack, + containerColor = TangemTheme.colors.background.tertiary, + title = { + if (state is AllOffersStateUM.Content && state.currentMethod != null) { + ProviderTitle( + onCloseClick = onCloseClick, + onBackClick = onBack, + ) + } else { + PaymentMethodTitle(onCloseClick = onCloseClick) + } + }, + content = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 8.dp) + .animateContentSize(), + ) { + AnimatedContent( + targetState = state is AllOffersStateUM.Content && state.currentMethod != null, + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + label = "Change offers and payment method state", + ) { shouldShowOffersScreen -> + when (state) { + AllOffersStateUM.Loading -> AllOffersContentLoading() + is AllOffersStateUM.Error -> AllOffersError(state.errorNotification) + is AllOffersStateUM.Content -> { + if (shouldShowOffersScreen) { + state.currentMethod?.let { + OffersBasedOnPaymentMethodContent(offers = it.offers) + } + } else { + PaymentMethodsContent(methods = state.methods) + } + } + } + } + } + }, + ) +} + +@Composable +private fun ProviderTitle(onBackClick: () -> Unit, onCloseClick: () -> Unit) { + TangemModalBottomSheetTitle( + title = TextReference.Res(R.string.onramp_all_offers_button_title), + subtitle = TextReference.Res(R.string.express_choose_providers_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = com.tangem.core.ui.R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) +} + +@Composable +private fun PaymentMethodTitle(onCloseClick: () -> Unit) { + TangemModalBottomSheetTitle( + title = TextReference.Res(R.string.onramp_all_offers_button_title), + subtitle = TextReference.Res(R.string.onramp_payment_method_subtitle), + endIconRes = com.tangem.core.ui.R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) +} + +@Composable +private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + offers.fastForEach { offer -> + key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") { + Offer(offer) + SpacerH(8.dp) + } + } + } +} + +@Composable +private fun AllOffersContentLoading() { + Column( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RectangleShimmer( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + .height(96.dp) + .size(width = 76.dp, height = 20.dp), + radius = 14.dp, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + .height(96.dp) + .size(width = 76.dp, height = 20.dp), + radius = 14.dp, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + .height(96.dp) + .size(width = 76.dp, height = 20.dp), + radius = 14.dp, + ) + } +} + +@Composable +fun AllOffersError(errorNotification: NotificationUM) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Notification(config = errorNotification.config) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AllOffersContentSheetPaymentPreview() { + val method = AllOffersPaymentMethodUM( + offers = persistentListOf( + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.BestRate, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId1", + providerName = "Simplex", + rate = "0,0245334 BTC", + diff = null, + onBuyClicked = {}, + ), + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.Default, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId2", + providerName = "Simplex", + rate = "0,00145334 BTC", + diff = stringReference("–0.07%"), + onBuyClicked = {}, + ), + ), + methodConfig = OnrampPaymentMethodConfig( + method = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + onClick = {}, + ), + diff = null, + rate = "0,0245334 BTC", + providersCount = 2, + isBestRate = true, + ) + + TangemThemePreview { + AllOffersContentSheet( + state = AllOffersStateUM.Content( + methods = persistentListOf(), + currentMethod = method, + onBackClicked = {}, + ), + onCloseClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AllOffersContentSheetOffersPreview() { + val methods = List(5) { + AllOffersPaymentMethodUM( + offers = persistentListOf( + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.BestRate, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = + "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId1", + providerName = "Simplex", + rate = "0,0245334 BTC", + diff = null, + onBuyClicked = {}, + ), + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.Default, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = + "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId2", + providerName = "Simplex", + rate = "0,00145334 BTC", + diff = stringReference("–0.07%"), + onBuyClicked = {}, + ), + ), + methodConfig = OnrampPaymentMethodConfig( + method = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + onClick = {}, + ), + diff = null, + rate = "0,0245334 BTC", + providersCount = 2, + isBestRate = true, + ) + } + + TangemThemePreview { + AllOffersContentSheet( + state = AllOffersStateUM.Content( + methods = methods.toPersistentList(), + currentMethod = null, + onBackClicked = {}, + ), + onCloseClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt new file mode 100644 index 0000000000..49d457c4b2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -0,0 +1,262 @@ +package com.tangem.features.onramp.alloffers.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodType +import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM +import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.mainv2.ui.TimingBlock +import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PaymentMethodsContent(methods: ImmutableList) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + methods.fastForEach { method -> + key(method.methodConfig.method.id) { + PaymentMethod(methodUM = method) + SpacerH(8.dp) + } + } + } +} + +@Composable +private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(14.dp), + ) + .clickable( + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = methodUM.methodConfig.onClick, + ) + .padding( + start = 12.dp, + end = 12.dp, + top = 14.dp, + bottom = 12.dp, + ), + ) { + PaymentMethodIcon( + modifier = Modifier.size(36.dp), + imageUrl = methodUM.methodConfig.method.imageUrl, + ) + + SpacerW(12.dp) + + Column { + PaymentMethodInfoBlock( + paymentMethodName = methodUM.methodConfig.method.name, + rate = methodUM.rate, + diff = methodUM.diff, + isBestRate = methodUM.isBestRate, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + ProvidersCountBlockInfo(providersCount = methodUM.providersCount) + SpacerW(8.dp) + TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed()) + } + } + } +} + +@Composable +private fun PaymentMethodInfoBlock( + paymentMethodName: String, + rate: String, + diff: TextReference?, + isBestRate: Boolean, +) { + Column(modifier = Modifier.padding(bottom = 14.dp)) { + Text( + text = paymentMethodName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + SpacerH(2.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResourceSafe(R.string.onramp_up_to_rate), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + + Text( + text = rate, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + + when { + isBestRate -> { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12), + contentDescription = null, + ) + } + diff != null -> { + Text( + modifier = Modifier + .background( + color = TangemTheme.colors.text.warning.copy(alpha = 0.1f), + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 4.dp), + text = diff.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.warning, + ) + } + } + } + } +} + +@Composable +private fun ProvidersCountBlockInfo(providersCount: Int) { + BorderedRow { + Icon( + modifier = Modifier.size(10.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerW(4.dp) + Text( + text = pluralStringResourceSafe( + id = R.plurals.onramp_providers_count, + count = providersCount, + providersCount, + ), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) { + BorderedRow { + Icon( + modifier = Modifier.size(10.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerW(4.dp) + TimingBlock(speed) + } +} + +@Composable +fun BorderedRow(content: @Composable RowScope.() -> Unit) { + Row( + modifier = Modifier + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } +} + +@Preview +@Composable +private fun PaymentMethodsContentPreview() { + val method = AllOffersPaymentMethodUM( + offers = persistentListOf( + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.BestRate, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId1", + providerName = "Simplex", + rate = "0,0245334 BTC", + diff = null, + onBuyClicked = {}, + ), + OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.Default, + paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + providerId = "providerId2", + providerName = "Simplex", + rate = "0,00145334 BTC", + diff = stringReference("–0.07%"), + onBuyClicked = {}, + ), + ), + methodConfig = OnrampPaymentMethodConfig( + method = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, + ), + onClick = {}, + ), + diff = null, + rate = "0,0245334 BTC", + providersCount = 2, + isBestRate = true, + ) + TangemThemePreview { + PaymentMethodsContent(persistentListOf(method)) + } +} \ No newline at end of file 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 index 2487bf37b1..af62d7ecbe 100644 --- 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 @@ -69,12 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor( onDismiss = model.bottomSheetNavigation::dismiss, ), ) - OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( + is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( context = childByContext(componentContext), params = AllOffersComponent.Params( userWallet = model.userWallet, cryptoCurrency = params.cryptoCurrency, onDismiss = model.bottomSheetNavigation::dismiss, + openRedirectPage = params.openRedirectPage, + amountCurrencyCode = config.amountCurrencyCode, ), ) } 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 index 6d20f87cf7..84fb039ffd 100644 --- 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 @@ -16,5 +16,5 @@ internal interface OnrampMainV2ComponentModelModule { @Binds @IntoMap @ClassKey(OnrampV2MainComponentModel::class) - fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model + fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model } \ 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 index d9e6c47a2d..afc654a422 100644 --- 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 @@ -12,5 +12,5 @@ sealed interface OnrampV2MainBottomSheetConfig { data object CurrenciesList : OnrampV2MainBottomSheetConfig @Serializable - data object AllOffers : OnrampV2MainBottomSheetConfig + data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig } \ 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 index 2f8f0f4186..e0947e34b5 100644 --- 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 @@ -45,6 +45,7 @@ internal class OnrampV2AmountFieldChangeConverter( continueButtonConfig = state.continueButtonConfig.copy(enabled = false), onrampProviderState = OnrampV2ProvidersUM.Loading, onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, ) } 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 index d83a94d1f0..abead667d7 100644 --- 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 @@ -118,7 +118,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( } override fun openSettings() { - params.openSettings + params.openSettings.invoke() } override fun openCurrenciesList() { @@ -139,7 +139,9 @@ internal class OnrampV2MainComponentModel @Inject constructor( } override fun openProviders() { - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers) + val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return + val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code + bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode)) } override fun onRefresh() { 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 index 1fc602096c..05645f62d0 100644 --- 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 @@ -12,6 +12,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache @@ -80,9 +81,11 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { SpacerH(8.dp) - state.recommended.fastForEach { - Offer(it) - SpacerH(8.dp) + state.recommended.fastForEach { offer -> + key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") { + Offer(offer) + SpacerH(8.dp) + } } state.onrampAllOffersButtonConfig?.let { @@ -99,7 +102,7 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { } @Composable -private fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) { +internal fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxWidth() @@ -263,7 +266,7 @@ private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName } @Composable -private fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) { +internal fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) { val timingText = when (speed) { PaymentMethodType.PaymentSpeed.Instant -> { stringResourceSafe(id = R.string.onramp_instant_status) From 700357c26b518fb31fb23750715bc6617ab1ec81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 15:46:25 +0300 Subject: [PATCH 62/69] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 6 + .../tangem/common/constants/TestConstants.kt | 2 + .../{steps => scenarios}/BaseScenarios.kt | 20 +- .../tangem/scenarios/DeepLinksScenarios.kt | 26 +++ .../com/tangem/scenarios/DialogScenarios.kt | 58 ++++++ .../scenarios/OpenMainScreenScenario.kt | 41 ---- .../WalletConnectScenarios.kt | 2 +- .../AlreadyUsedWalletDialogPageObject.kt | 32 ++++ .../com/tangem/screens/DialogPageObject.kt | 4 +- .../FailedTransactionDialogPageObject.kt | 44 +++++ .../tangem/screens/MainScreenPageObject.kt | 2 +- .../screens/ScanWarningDialogPageObject.kt | 36 ++++ .../tangem/screens/SendAddressPageObject.kt | 30 +++ .../tangem/screens/SendConfirmPageObject.kt | 30 +++ ...ingSendPageObject.kt => SendPageObject.kt} | 30 +-- ...eObject.kt => StakingConfirmPageObject.kt} | 11 +- .../tangem/screens/TokenDetailsPageObject.kt | 6 + .../com/tangem/steps/DeepLinksScenarios.kt | 15 -- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 57 ++---- .../kotlin/com/tangem/tests/DetailsTest.kt | 18 +- .../kotlin/com/tangem/tests/FeedbackTest.kt | 176 ++++++++++++++++++ .../kotlin/com/tangem/tests/HideTokenTest.kt | 12 +- .../kotlin/com/tangem/tests/MainScreenTest.kt | 6 +- .../com/tangem/tests/OrganizeTokensTest.kt | 29 ++- .../kotlin/com/tangem/tests/StakingTest.kt | 125 +++++++------ .../kotlin/com/tangem/tests/SwapTokenTest.kt | 32 ++-- .../com/tangem/tests/WalletConnectTest.kt | 5 +- .../common/ui/amountScreen/ui/AmountBlock.kt | 6 +- .../ui/amountScreen/ui/AmountButtons.kt | 10 +- .../common/ui/amountScreen/ui/AmountField.kt | 4 +- .../amountScreen/ui/AmountFieldContainer.kt | 6 +- .../NavigationButtonsBlock.kt | 4 +- .../com/tangem/core/ui/components/Dialogs.kt | 10 +- .../ui/components/fields/AmountTextField.kt | 4 +- .../components/inputrow/InputRowRecipient.kt | 6 +- .../core/ui/test/BaseAmountBlockTestTags.kt | 6 + .../tangem/core/ui/test/BaseDialogTestTags.kt | 7 + .../com/tangem/core/ui/test/DialogTestTags.kt | 5 - .../core/ui/test/SendAddressScreenTestTags.kt | 6 + .../tangem/core/ui/test/SendScreenTestTags.kt | 16 ++ .../test/StakingSendDetailsScreenTestTags.kt | 8 +- .../core/ui/test/StakingSendScreenTestTags.kt | 16 -- .../impl/presentation/ui/StakingScreen.kt | 4 +- tangem-android-tools | 2 +- 44 files changed, 683 insertions(+), 292 deletions(-) rename app/src/androidTest/kotlin/com/tangem/{steps => scenarios}/BaseScenarios.kt (70%) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/DeepLinksScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt rename app/src/androidTest/kotlin/com/tangem/{steps => scenarios}/WalletConnectScenarios.kt (99%) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AlreadyUsedWalletDialogPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/FailedTransactionDialogPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt rename app/src/androidTest/kotlin/com/tangem/screens/{StakingSendPageObject.kt => SendPageObject.kt} (61%) rename app/src/androidTest/kotlin/com/tangem/screens/{StakingSendDetailsPageObject.kt => StakingConfirmPageObject.kt} (75%) delete mode 100644 app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BaseAmountBlockTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SendAddressScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 7ab38e88e2..ae8dab772f 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -19,6 +19,8 @@ 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 +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.models.PromoId import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import kotlinx.coroutines.runBlocking @@ -51,6 +53,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var featureTogglesManager: FeatureTogglesManager + @Inject + lateinit var promoRepository: PromoRepository + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -91,6 +96,7 @@ abstract class BaseTestCase : TestCase( value = false ) } + promoRepository.setNeverToShowWalletPromo(PromoId.Sepa) } apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index 00cfc3afe3..a985597ee8 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -3,5 +3,7 @@ package com.tangem.common.constants object TestConstants { const val TOTAL_BALANCE = "$3,299.18" + const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0" + const val WAIT_UNTIL_TIMEOUT = 20_000L } \ 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/scenarios/BaseScenarios.kt similarity index 70% rename from app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt rename to app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 54f4f8a915..8355c58267 100644 --- a/app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -1,29 +1,31 @@ -package com.tangem.steps +package com.tangem.scenarios 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.screens.* import com.tangem.tap.domain.sdk.mocks.MockProvider import io.qameta.allure.kotlin.Allure.step -fun BaseTestCase.openMainScreen(productType: ProductType? = null) { +fun BaseTestCase.openMainScreen(productType: ProductType? = null, alreadyActivatedDialogIsShown : Boolean = false) { if (productType != null) { MockProvider.setMocks(productType) } step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } } - step("Click on 'Accept' button") { + step("Click on 'Scan' button") { onStoriesScreen { scanButton.clickWithAssertion() } } - step("Assert main screen is displayed") { + if (alreadyActivatedDialogIsShown) { + step("Click on 'This is my wallet' button") { + AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() } + } + } + step("Assert 'Main' screen is displayed") { onMainScreen { screenContainer.assertIsDisplayed() } } - step("Assert main screen is displayed") { + step("Click on 'Market Tooltip' screen") { onMarketsTooltipScreen { contentContainer.clickWithAssertion() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DeepLinksScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DeepLinksScenarios.kt new file mode 100644 index 0000000000..e141a7cc1c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DeepLinksScenarios.kt @@ -0,0 +1,26 @@ +package com.tangem.scenarios + +import android.content.Intent +import android.content.Intent.* +import android.net.Uri +import androidx.test.core.app.ApplicationProvider +import io.github.kakaocup.kakao.intent.KIntent + +fun openAppByDeepLink(deepLinkUri: String?) { + val deeplinkScheme = "tangem://wc?uri=" + val context = ApplicationProvider.getApplicationContext() + val intent = Intent(ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply { + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + + context.startActivity(intent) +} + +fun checkSendEMailIntentCalled() { + val expectedIntent = KIntent { + hasAction(ACTION_CHOOSER) + hasExtra(EXTRA_TITLE, "Send mail...") + hasExtraWithKey(EXTRA_INTENT) + } + expectedIntent.intended() +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt new file mode 100644 index 0000000000..0b7990da23 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt @@ -0,0 +1,58 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.screens.AlreadyUsedWalletDialogPageObject +import com.tangem.screens.ScanWarningDialogPageObject +import com.tangem.screens.onFailedTransactionDialog +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.checkFailedTransactionDialog() { + step("Assert failed transaction dialog title is displayed") { + onFailedTransactionDialog { title.assertIsDisplayed() } + } + step("Assert failed transaction dialog text is displayed") { + onFailedTransactionDialog { text.assertIsDisplayed() } + } + step("Assert 'Cancel' button is displayed") { + onFailedTransactionDialog { cancelButton.assertIsDisplayed() } + } + step("Assert 'Support' button is displayed") { + onFailedTransactionDialog { supportButton.assertIsDisplayed() } + } +} + +fun checkScanWarningDialog() { + step("Assert 'Scan warning' dialog title is displayed") { + ScanWarningDialogPageObject { warningTitle.isDisplayed() } + } + step("Assert warning dialog message is displayed") { + ScanWarningDialogPageObject { warningMessage.isDisplayed() } + } + step("Assert 'Cancel' button is displayed") { + ScanWarningDialogPageObject { cancelButton.isDisplayed() } + } + step("Assert 'How to scan' button is displayed") { + ScanWarningDialogPageObject { howToScanButton.isDisplayed() } + } + step("Assert 'Request support' button is displayed") { + ScanWarningDialogPageObject { requestSupportButton.isDisplayed() } + } +} + +fun checkAlreadyUsedWalletDialog() { + step("Assert 'Already used Wallet' dialog title is displayed") { + AlreadyUsedWalletDialogPageObject { title.isDisplayed() } + } + step("Assert dialog message is displayed") { + AlreadyUsedWalletDialogPageObject { message.isDisplayed() } + } + step("Assert 'This is my wallet' button is displayed") { + AlreadyUsedWalletDialogPageObject { message.isDisplayed() } + } + step("Assert 'Cancel' button is displayed") { + AlreadyUsedWalletDialogPageObject { cancelButton.isDisplayed() } + } + step("Assert 'Request support' button is displayed") { + AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt deleted file mode 100644 index dd160f1b25..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.scenarios - -import androidx.compose.ui.test.junit4.ComposeTestRule -import com.kaspersky.kaspresso.testcases.api.scenario.Scenario -import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext -import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.models.scan.ProductType -import com.tangem.screens.* -import com.tangem.tap.domain.sdk.mocks.MockProvider -import io.github.kakaocup.compose.node.element.ComposeScreen - -class OpenMainScreenScenario( - private val testRule: ComposeTestRule, - private val productType: ProductType? = null, -) : Scenario() { - override val steps: TestContext.() -> Unit = { - if (productType != null) { - MockProvider.setMocks(productType) - } - ComposeScreen.onComposeScreen(testRule) { - step("Click on \"Accept\" button") { - acceptButton.clickWithAssertion() - } - } - ComposeScreen.onComposeScreen(testRule) { - step("Click on \"Scan\" button") { - scanButton.clickWithAssertion() - } - } - ComposeScreen.onComposeScreen(testRule) { - step("Make sure wallet screen is visible") { - assertIsDisplayed() - } - } - ComposeScreen.onComposeScreen(testRule) { - step("Close Markets tooltip"){ - contentContainer.performClick() - } - } - } -} \ 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/scenarios/WalletConnectScenarios.kt similarity index 99% rename from app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt rename to app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt index 77134c2ea3..107806004a 100644 --- a/app/src/androidTest/kotlin/com/tangem/steps/WalletConnectScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt @@ -1,4 +1,4 @@ -package com.tangem.steps +package com.tangem.scenarios import com.tangem.common.BaseTestCase import com.tangem.screens.onWalletConnectBottomSheet diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AlreadyUsedWalletDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AlreadyUsedWalletDialogPageObject.kt new file mode 100644 index 0000000000..7f5e42cc48 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AlreadyUsedWalletDialogPageObject.kt @@ -0,0 +1,32 @@ +package com.tangem.screens + +import com.kaspersky.kaspresso.screens.KScreen +import com.tangem.wallet.R +import io.github.kakaocup.kakao.text.KTextView +import io.github.kakaocup.kakao.text.KButton + +object AlreadyUsedWalletDialogPageObject : KScreen() { + + override val layoutId: Int? = null + override val viewClass: Class<*>? = null + + val title = KTextView { + withText(R.string.security_alert_title) + } + + val message = KTextView { + withText(R.string.wallet_been_activated_message) + } + + val cancelButton = KButton { + withText(R.string.common_cancel) + } + + val requestSupportButton = KButton { + withText(R.string.alert_button_request_support) + } + + val thisIsMyWalletButton = KButton { + withText(R.string.this_is_my_wallet_title) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 5bac035afc..e60f497bc3 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -4,7 +4,7 @@ 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.DialogTestTags +import com.tangem.core.ui.test.BaseDialogTestTags 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 @@ -14,7 +14,7 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { val dialogContainer: KNode = child { - hasTestTag(DialogTestTags.DIALOG_CONTAINER) + hasTestTag(BaseDialogTestTags.CONTAINER) } val cancelButton: KNode = child { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/FailedTransactionDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/FailedTransactionDialogPageObject.kt new file mode 100644 index 0000000000..4a00a7e1b8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/FailedTransactionDialogPageObject.kt @@ -0,0 +1,44 @@ +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.BaseDialogTestTags +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.common.ui.R as CommonUIR + +class FailedTransactionDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val dialogContainer: KNode = child { + hasTestTag(BaseDialogTestTags.CONTAINER) + } + + val title: KNode = child { + hasTestTag(BaseDialogTestTags.TITLE) + hasText(getResourceString(CommonUIR.string.send_alert_transaction_failed_title)) + useUnmergedTree = true + } + + val text: KNode = child { + hasTestTag(BaseDialogTestTags.TEXT) + useUnmergedTree = true + } + + val cancelButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_cancel)) + } + + val supportButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_support)) + } +} + +internal fun BaseTestCase.onFailedTransactionDialog(function: FailedTransactionDialogPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ 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 56742890d9..59419f5f41 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -39,7 +39,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasTestTag(MainScreenTestTags.SCREEN_CONTAINER) } - val synchronizeAddressesButton: KNode = child { + val synchronizeAddressesButton: KNode = lazyList.child { hasText(getResourceString(R.string.common_generate_addresses)) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt new file mode 100644 index 0000000000..245f08e61d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt @@ -0,0 +1,36 @@ +package com.tangem.screens + +import com.kaspersky.kaspresso.screens.KScreen +import com.tangem.wallet.R +import io.github.kakaocup.kakao.text.KTextView +import io.github.kakaocup.kakao.text.KButton + +object ScanWarningDialogPageObject : KScreen() { + + override val layoutId: Int? = null + override val viewClass: Class<*>? = null + + val warningTitle = KTextView { + withText(R.string.common_warning) + } + + val warningMessage = KTextView { + withText(R.string.alert_troubleshooting_scan_card_title) + } + + val tryAgainButton = KButton { + withId(R.id.try_again_button) + } + + val howToScanButton = KButton { + withId(R.id.how_to_scan_button) + } + + val requestSupportButton = KButton { + withId(R.id.request_support_button) + } + + val cancelButton = KButton { + withId(R.id.cancel_button) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt new file mode 100644 index 0000000000..8186ff1683 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt @@ -0,0 +1,30 @@ +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.SendAddressScreenTestTags +import com.tangem.features.send.v2.impl.R +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 SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val addressTextField: KNode = child { + hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD) + useUnmergedTree = true + } + + val nextButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_next)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onSendAddressScreen(function: SendAddressPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt new file mode 100644 index 0000000000..900ead781c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendConfirmPageObject.kt @@ -0,0 +1,30 @@ +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.TopAppBarTestTags +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 SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val sendButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_send)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt similarity index 61% rename from app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt rename to app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt index f71c9ad210..f988ad6de5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt @@ -3,7 +3,7 @@ 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.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.core.ui.test.TopAppBarTestTags import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen @@ -12,11 +12,11 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString import com.tangem.features.send.v2.impl.R as SendR import androidx.compose.ui.test.hasTestTag as withTestTag -class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen(semanticsProvider = semanticsProvider) { +class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { val screenContainer: KNode = child { - hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER) + hasTestTag(SendScreenTestTags.SCREEN_CONTAINER) } val title: KNode = child { @@ -25,44 +25,44 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider } val amountContainerTitle: KNode = child { - hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE) + hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE) useUnmergedTree = true } val amountContainerText: KNode = child { - hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT) + hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT) useUnmergedTree = true } val amountInputTextField: KNode = child { - hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD) + hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD) useUnmergedTree = true } val secondaryAmount: KNode = child { - hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT) + hasTestTag(SendScreenTestTags.SECONDARY_AMOUNT) useUnmergedTree = true } val currencyButton: KNode = child { - hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) - hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON)) + hasTestTag(SendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(SendScreenTestTags.CURRENCY_ICON)) useUnmergedTree = true } val fiatButton: KNode = child { - hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) - hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON)) + hasTestTag(SendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(SendScreenTestTags.FIAT_ICON)) useUnmergedTree = true } val maxButton: KNode = child { - hasTestTag(StakingSendScreenTestTags.MAX_BUTTON) + hasTestTag(SendScreenTestTags.MAX_BUTTON) useUnmergedTree = true } val previousButton: KNode = child { - hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON) + hasTestTag(SendScreenTestTags.PREVIOUS_BUTTON) useUnmergedTree = true } @@ -74,5 +74,5 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider } -internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) = +internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) = onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingConfirmPageObject.kt similarity index 75% rename from app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt rename to app/src/androidTest/kotlin/com/tangem/screens/StakingConfirmPageObject.kt index 79b38d7c9c..e9a7eb2983 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingConfirmPageObject.kt @@ -3,6 +3,7 @@ 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.BaseAmountBlockTestTags import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.core.ui.test.TopAppBarTestTags @@ -11,8 +12,8 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString -class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen(semanticsProvider = semanticsProvider) { +class StakingConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { val title: KNode = child { hasTestTag(TopAppBarTestTags.TITLE) @@ -20,12 +21,12 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP } val primaryAmount: KNode = child { - hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT) + hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT) useUnmergedTree = true } val secondaryAmount: KNode = child { - hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT) + hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT) useUnmergedTree = true } @@ -47,5 +48,5 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP } -internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) = +internal fun BaseTestCase.onStakingConfirmScreen(function: StakingConfirmPageObject.() -> Unit) = onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 128a24ad03..d17b8a09c2 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -111,6 +111,12 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide hasText(getResourceString(R.string.common_buy)) } + @OptIn(ExperimentalTestApi::class) + val sendButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_send)) + } + } internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt b/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt deleted file mode 100644 index 832a1b9fe8..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/steps/DeepLinksScenarios.kt +++ /dev/null @@ -1,15 +0,0 @@ -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/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index f553c1cbe0..abea9913ef 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -6,8 +6,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState -import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -36,13 +37,10 @@ class BuyTokenTest : BaseTestCase() { setWireMockScenarioState(scenarioName, "Error") } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } @@ -84,13 +82,10 @@ class BuyTokenTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } @@ -175,13 +170,10 @@ class BuyTokenTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } @@ -250,13 +242,10 @@ class BuyTokenTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } @@ -339,13 +328,10 @@ class BuyTokenTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } @@ -437,13 +423,10 @@ class BuyTokenTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on 'Buy' button") { onMainScreen { buyButton.clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index df318a9456..b60639cf4a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -3,7 +3,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType -import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.scenarios.openMainScreen import com.tangem.screens.onDetailsScreen import com.tangem.screens.onReferralProgramScreen import com.tangem.screens.onTopBar @@ -19,7 +19,9 @@ class DetailsTest : BaseTestCase() { @Test fun walletWithoutBackupDetailsTest() = setupHooks().run { - scenario(OpenMainScreenScenario(composeTestRule)) + step("Open 'Main Screen'") { + openMainScreen() + } onTopBar { step("Open wallet details") { moreButton.clickWithAssertion() @@ -67,7 +69,9 @@ class DetailsTest : BaseTestCase() { // @Test fun wallet2DetailsTest() = setupHooks().run { - scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) + step("Open 'Main Screen'") { + openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + } onTopBar { step("Open wallet details") { moreButton.clickWithAssertion() @@ -115,7 +119,9 @@ class DetailsTest : BaseTestCase() { @Test fun noteDetailsTest() = setupHooks().run { - scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note)) + step("Open 'Main Screen'") { + openMainScreen(ProductType.Note) + } onTopBar { step("Open wallet details") { moreButton.clickWithAssertion() @@ -162,7 +168,9 @@ class DetailsTest : BaseTestCase() { @Test fun validateReferralProgramScreenTest() = setupHooks().run { - scenario(OpenMainScreenScenario(composeTestRule)) + step("Open 'Main Screen'") { + openMainScreen() + } step("Open wallet details") { onTopBar { moreButton.clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt new file mode 100644 index 0000000000..f2426d8093 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -0,0 +1,176 @@ +package com.tangem.tests + +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.redux.StateDialog +import com.tangem.scenarios.* +import com.tangem.screens.* +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.sdk.mocks.MockProvider +import com.tangem.tap.store +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.Allure +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore +import org.junit.Test + +@HiltAndroidTest +class FeedbackTest : BaseTestCase() { + + @AllureId("894") + @DisplayName("Send feedback: from details") + @Test + fun sendFeedbackFromDetailsTest() { + setupHooks( + additionalAfterSection = { + device.uiDevice.pressBack() + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click 'Contact support' button") { + onDetailsScreen { contactSupportButton.clickWithAssertion() } + } + step("Check 'Contact support' intent is called") { + checkSendEMailIntentCalled() + } + } + } + + @AllureId("893") + @DisplayName("Send feedback: failed transaction") + @Test + fun sendFeedbackFromFailedTransactionTest() { + val balance = TOTAL_BALANCE + val tokenName = "Polygon" + val recipientAddress = RECIPIENT_ADDRESS + val sendAmount = "1" + + setupHooks( + additionalAfterSection = { + device.uiDevice.pressBack() + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Click 'Send' button") { + onTokenDetailsScreen { sendButton.performClick() } + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Assert input text field has value: '$sendAmount'") { + onSendScreen { amountInputTextField.assertTextContains(value = sendAmount, substring = true) } + } + step("Click 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Enter address") { + onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) } + } + step("Click 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Click 'Send' button") { + onSendConfirmScreen { sendButton.clickWithAssertion() } + } + step("Check 'Failed transaction' dialog") { + checkFailedTransactionDialog() + } + step("Click on 'Support' button") { + onFailedTransactionDialog { supportButton.performClick() } + } + step("Check 'Contact support' intent is called") { + checkSendEMailIntentCalled() + } + } + } + + @AllureId("3985") + @DisplayName("Send feedback: from 'Warning' dialog after card scan") + @Test + fun sendFeedbackFromScanScreenTest() { + setupHooks( + additionalAfterSection = { + device.uiDevice.pressBack() + MockProvider.resetEmulateError() + } + ).run { + Allure.step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Set scanning error") { + MockProvider.setEmulateError(TangemSdkError.TagLost()) + } + step("Click on 'Scan' button") { + onStoriesScreen { scanButton.performClick() } + } + step("Force show 'Scan warning' dialog"){ + runOnUiThread { + val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN) + store.dispatch(GlobalAction.ShowDialog(scanFailsState)) + } + } + step("Check 'Scan warning' dialog") { + checkScanWarningDialog() + } + step("Click on 'Request support' button") { + ScanWarningDialogPageObject { requestSupportButton.click() } + } + step("Check 'Contact support' intent is called") { + checkSendEMailIntentCalled() + } + } + } + + @AllureId("3986") + @DisplayName("Send feedback: from scan already used wallet alert dialog") + @Ignore("TODO On CI Already used wallet doesn't displayed") + @Test + fun sendFeedbackAfterScanAlreadyUsedWalletTest() { + setupHooks( + additionalAfterSection = { + device.uiDevice.pressBack() + } + ).run { + step("Set mocks for Wallet2") { + MockProvider.setMocks(ProductType.Wallet2) + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Click on 'Scan' button") { + onStoriesScreen { scanButton.clickWithAssertion() } + } + step("Check 'Already used Wallet' dialog") { + checkAlreadyUsedWalletDialog() + } + step("Click on 'Request support' button") { + AlreadyUsedWalletDialogPageObject { requestSupportButton.click() } + } + step("Check 'Contact support' intent is called") { + checkSendEMailIntentCalled() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt index 706bd497d5..9e1efb911a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt @@ -3,8 +3,9 @@ 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.scenarios.OpenMainScreenScenario import com.tangem.screens.* +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -21,13 +22,10 @@ class HideTokenTest : BaseTestCase() { val balance = TOTAL_BALANCE setupHooks().run { step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button" ) { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt index c708b29087..cf7f5747e6 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt @@ -1,7 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.scenarios.openMainScreen import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -11,7 +11,9 @@ class MainScreenTest : BaseTestCase() { @Test fun goToMain() { setupHooks().run { - scenario(OpenMainScreenScenario(composeTestRule)) + step("Open 'Main Screen'") { + openMainScreen() + } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 76d8e34a7a..e0f1d97826 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -5,9 +5,10 @@ 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.scenarios.OpenMainScreenScenario import com.tangem.screens.onMainScreen import com.tangem.screens.onOrganizeTokensScreen +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -23,8 +24,9 @@ class OrganizeTokensTest : BaseTestCase() { setupHooks().run { val tokenTitle = "Ethereum" val tokenNetwork = "Ethereum network" + step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } @@ -90,14 +92,12 @@ class OrganizeTokensTest : BaseTestCase() { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" val balance = TOTAL_BALANCE + step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button" ) { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Check positions of tokens on 'Main Screen'") { onMainScreen { @@ -129,8 +129,9 @@ class OrganizeTokensTest : BaseTestCase() { setupHooks().run { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" + step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } @@ -178,14 +179,12 @@ class OrganizeTokensTest : BaseTestCase() { val polygonTitle = "Polygon" val polExMaticTitle = "POL (ex-MATIC)" val balance = TOTAL_BALANCE + step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button" ) { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = '$balance'") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Check positions of tokens on 'Main Screen'") { onMainScreen { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt index 083478f475..5eb2c98591 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -5,8 +5,9 @@ import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState -import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -20,7 +21,7 @@ class StakingTest : BaseTestCase() { @Test fun validateStakingBlockTest() { val tokenTitle = "POL (ex-MATIC)" - val balance = TOTAL_BALANCE + val balance = "$3,299.37" val scenarioName = "staking_eth_pol_balances_android" val scenarioState = "Staked" @@ -35,13 +36,13 @@ class StakingTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + step("Synchronize addresses") { + synchronizeAddresses(balance) } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Assert 'Organize tokens' button is displayed") { + onMainScreen { organizeTokensButton().assertIsDisplayed() } } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } @@ -75,7 +76,7 @@ class StakingTest : BaseTestCase() { @Test fun validateStakingMoreScreensTest() { val tokenTitle = "POL (ex-MATIC)" - val balance = TOTAL_BALANCE + val balance = "$3,299.37" val scenarioName = "staking_eth_pol_balances_android" val scenarioState = "Staked" val stakingAmount = "1" @@ -91,13 +92,13 @@ class StakingTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + step("Synchronize addresses") { + synchronizeAddresses(balance) } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Assert 'Organize tokens' button is displayed") { + onMainScreen { organizeTokensButton().assertIsDisplayed() } } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } @@ -151,73 +152,73 @@ class StakingTest : BaseTestCase() { onStakingDetailsScreen { stakeMoreButton.performClick() } } step("Assert 'Send' screen is displayed") { - onStakingSendScreen { screenContainer.assertIsDisplayed() } + onSendScreen { screenContainer.assertIsDisplayed() } } step("Assert 'Send' screen title is displayed") { - onStakingSendScreen { title.assertIsDisplayed() } + onSendScreen { title.assertIsDisplayed() } } step("Assert amount container title is displayed") { - onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + onSendScreen { amountContainerTitle.assertIsDisplayed() } } step("Assert amount container text is displayed") { - onStakingSendScreen { amountContainerText.assertIsDisplayed() } + onSendScreen { amountContainerText.assertIsDisplayed() } } step("Assert input text field is displayed") { - onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + onSendScreen { amountInputTextField.assertIsDisplayed() } } step("Assert secondary amount is displayed") { - onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + onSendScreen { secondaryAmount.assertIsDisplayed() } } step("Type '$stakingAmount' in input text field") { - onStakingSendScreen { + onSendScreen { amountInputTextField.performClick() amountInputTextField.performTextReplacement(stakingAmount) } } step("Assert input text field has value: '$stakingAmount'") { - onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } } step("Assert currency button is displayed") { - onStakingSendScreen { currencyButton.assertIsDisplayed() } + onSendScreen { currencyButton.assertIsDisplayed() } } step("Assert fiat button is displayed") { - onStakingSendScreen { fiatButton.assertIsDisplayed() } + onSendScreen { fiatButton.assertIsDisplayed() } } step("Assert currency button is displayed") { - onStakingSendScreen { currencyButton.assertIsDisplayed() } + onSendScreen { currencyButton.assertIsDisplayed() } } step("Assert fiat button is displayed") { - onStakingSendScreen { fiatButton.assertIsDisplayed() } + onSendScreen { fiatButton.assertIsDisplayed() } } step("Assert 'Max' button is displayed") { - onStakingSendScreen { maxButton.assertIsDisplayed() } + onSendScreen { maxButton.assertIsDisplayed() } } step("Assert previous button is displayed") { - onStakingSendScreen { previousButton.assertIsDisplayed() } + onSendScreen { previousButton.assertIsDisplayed() } } step("Assert 'Next' button is displayed") { - onStakingSendScreen { nextButton.assertIsDisplayed() } + onSendScreen { nextButton.assertIsDisplayed() } } step("Click on 'Next' button") { - onStakingSendScreen { nextButton.performClick() } + onSendScreen { nextButton.performClick() } } step("Assert 'Send details' screen title is displayed") { - onStakingSendDetailsScreen { title.assertIsDisplayed() } + onStakingConfirmScreen { title.assertIsDisplayed() } } step("Assert primary amount is displayed") { - onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + onStakingConfirmScreen { primaryAmount.assertIsDisplayed() } } step("Assert secondary amount is displayed") { - onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() } } step("Assert 'Validator' block is displayed") { - onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + onStakingConfirmScreen { validatorBlock.assertIsDisplayed() } } step("Assert 'Network Fee' block is displayed") { - onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() } } step("Assert 'Stake' button is displayed") { - onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + onStakingConfirmScreen { stakeButton.assertIsDisplayed() } } } } @@ -243,13 +244,13 @@ class StakingTest : BaseTestCase() { } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + step("Synchronize addresses") { + synchronizeAddresses(balance) } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Assert 'Organize tokens' button is displayed") { + onMainScreen { organizeTokensButton().assertIsDisplayed() } } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } @@ -306,73 +307,73 @@ class StakingTest : BaseTestCase() { onStakingDetailsScreen { stakeButton.performClick() } } step("Assert 'Send' screen is displayed") { - onStakingSendScreen { screenContainer.assertIsDisplayed() } + onSendScreen { screenContainer.assertIsDisplayed() } } step("Assert 'Send' screen title is displayed") { - onStakingSendScreen { title.assertIsDisplayed() } + onSendScreen { title.assertIsDisplayed() } } step("Assert amount container title is displayed") { - onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + onSendScreen { amountContainerTitle.assertIsDisplayed() } } step("Assert amount container text is displayed") { - onStakingSendScreen { amountContainerText.assertIsDisplayed() } + onSendScreen { amountContainerText.assertIsDisplayed() } } step("Assert input text field is displayed") { - onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + onSendScreen { amountInputTextField.assertIsDisplayed() } } step("Assert secondary amount is displayed") { - onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + onSendScreen { secondaryAmount.assertIsDisplayed() } } step("Type '$stakingAmount' in input text field") { - onStakingSendScreen { + onSendScreen { amountInputTextField.performClick() amountInputTextField.performTextReplacement(stakingAmount) } } step("Assert input text field has value: '$stakingAmount'") { - onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } } step("Assert currency button is displayed") { - onStakingSendScreen { currencyButton.assertIsDisplayed() } + onSendScreen { currencyButton.assertIsDisplayed() } } step("Assert fiat button is displayed") { - onStakingSendScreen { fiatButton.assertIsDisplayed() } + onSendScreen { fiatButton.assertIsDisplayed() } } step("Assert currency button is displayed") { - onStakingSendScreen { currencyButton.assertIsDisplayed() } + onSendScreen { currencyButton.assertIsDisplayed() } } step("Assert fiat button is displayed") { - onStakingSendScreen { fiatButton.assertIsDisplayed() } + onSendScreen { fiatButton.assertIsDisplayed() } } step("Assert 'Max' button is displayed") { - onStakingSendScreen { maxButton.assertIsDisplayed() } + onSendScreen { maxButton.assertIsDisplayed() } } step("Assert previous button is displayed") { - onStakingSendScreen { previousButton.assertIsDisplayed() } + onSendScreen { previousButton.assertIsDisplayed() } } step("Assert 'Next' button is displayed") { - onStakingSendScreen { nextButton.assertIsDisplayed() } + onSendScreen { nextButton.assertIsDisplayed() } } step("Click on 'Next' button") { - onStakingSendScreen { nextButton.performClick() } + onSendScreen { nextButton.performClick() } } step("Assert 'Send details' screen title is displayed") { - onStakingSendDetailsScreen { title.assertIsDisplayed() } + onStakingConfirmScreen { title.assertIsDisplayed() } } step("Assert primary amount is displayed") { - onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + onStakingConfirmScreen { primaryAmount.assertIsDisplayed() } } step("Assert secondary amount is displayed") { - onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() } } step("Assert 'Validator' block is displayed") { - onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + onStakingConfirmScreen { validatorBlock.assertIsDisplayed() } } step("Assert 'Network Fee' block is displayed") { - onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() } } step("Assert 'Stake' button is displayed") { - onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + onStakingConfirmScreen { stakeButton.assertIsDisplayed() } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt index 638218b875..887bffc37a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt @@ -9,8 +9,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -32,13 +33,10 @@ class SwapTokenTest : BaseTestCase() { val balance = TOTAL_BALANCE step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } @@ -111,13 +109,10 @@ class SwapTokenTest : BaseTestCase() { val balance = TOTAL_BALANCE step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } @@ -149,7 +144,7 @@ class SwapTokenTest : BaseTestCase() { @ApiEnv( ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) ) - @AllureId("3546") + @AllureId("3547") @DisplayName("Swap: change network fee") @Test fun changeNetworkFeeTest() { @@ -159,13 +154,10 @@ class SwapTokenTest : BaseTestCase() { val balance = TOTAL_BALANCE step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } - step("Click on 'Synchronize addresses' button") { - onMainScreen { synchronizeAddressesButton.clickWithAssertion() } - } - step("Assert wallet balance = $balance") { - onMainScreen { walletBalance().assertTextContains(balance) } + step("Synchronize addresses") { + synchronizeAddresses(balance) } step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt index ff9f1239bd..670444ff59 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt @@ -5,9 +5,8 @@ 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 com.tangem.scenarios.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -147,7 +146,7 @@ class WalletConnectTest : BaseTestCase() { openAppByDeepLink(deepLinkUri) } step("Open 'Main Screen'") { - scenario(OpenMainScreenScenario(composeTestRule)) + openMainScreen() } step("Check 'Wallet Connect' bottom sheet") { checkWalletConnectBottomSheet() diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index 47418a9338..8e21c12dd9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags +import com.tangem.core.ui.test.BaseAmountBlockTestTags import java.math.BigDecimal @Composable @@ -66,7 +66,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis modifier = Modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing24) - .testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT), + .testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT), ) Text( text = secondAmount, @@ -76,7 +76,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis modifier = Modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing8) - .testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT), + .testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index 753a0fc99e..6263c12b93 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon 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.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags import kotlinx.collections.immutable.PersistentList private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" @@ -80,7 +80,7 @@ internal fun LazyListScope.buttons( vertical = TangemTheme.dimens.spacing10, horizontal = TangemTheme.dimens.spacing34, ) - .testTag(StakingSendScreenTestTags.MAX_BUTTON), + .testTag(SendScreenTestTags.MAX_BUTTON), ) } } @@ -94,7 +94,7 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment .padding( horizontal = TangemTheme.dimens.spacing10, ) - .testTag(StakingSendScreenTestTags.CURRENCY_BUTTON), + .testTag(SendScreenTestTags.CURRENCY_BUTTON), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { @@ -106,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment url = button.iconUrl, size = TangemTheme.dimens.size18, isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON), + modifier = iconModifier.testTag(SendScreenTestTags.FIAT_ICON), ) } else if (button.iconState != null) { CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, - modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON), + modifier = iconModifier.testTag(SendScreenTestTags.CURRENCY_ICON), ) } Text( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index f491629420..848bd0ab36 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -29,7 +29,7 @@ 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.core.ui.res.TangemTheme -import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @@ -119,7 +119,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri modifier = Modifier .align(TopCenter) .padding(bottom = TangemTheme.dimens.spacing32) - .testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT), + .testTag(SendScreenTestTags.SECONDARY_AMOUNT), ) AmountFieldError( isError = amountField.isError, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index d831aad665..6fd563f173 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -29,7 +29,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars 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.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -54,7 +54,7 @@ internal fun LazyListScope.amountField( color = TangemTheme.colors.text.tertiary, modifier = Modifier .padding(top = TangemTheme.dimens.spacing14) - .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE), + .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE), ) val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() @@ -69,7 +69,7 @@ internal fun LazyListScope.amountField( textAlign = TextAlign.Center, modifier = Modifier .padding(top = TangemTheme.dimens.spacing2) - .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT), + .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT), ) } CurrencyIcon( diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index b22f5341e4..496559367f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -36,7 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.singleEvent -import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags @Composable fun NavigationButtonsBlock( @@ -149,7 +149,7 @@ private fun PreviousButton(prevButton: NavigationButton?) { .background(TangemTheme.colors.button.secondary) .clickable(onClick = button.onClick) .padding(TangemTheme.dimens.spacing12) - .testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON), + .testTag(SendScreenTestTags.PREVIOUS_BUTTON), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index bf1fc3a926..b8955c8b80 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -30,7 +30,7 @@ import com.tangem.core.ui.components.fields.SimpleDialogTextField 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.DialogTestTags +import com.tangem.core.ui.test.BaseDialogTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -219,13 +219,14 @@ private fun TangemDialog( color = TangemTheme.colors.background.primary, ) .padding(vertical = TangemTheme.dimens.spacing24) - .testTag(DialogTestTags.DIALOG_CONTAINER), + .testTag(BaseDialogTestTags.CONTAINER), ) { if (title != null) { Text( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing24) - .fillMaxWidth(), + .fillMaxWidth() + .testTag(BaseDialogTestTags.TITLE), text = title, style = when (type) { is DialogType.Message -> TangemTheme.typography.h2 @@ -262,7 +263,8 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { Text( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing24) - .fillMaxWidth(), + .fillMaxWidth() + .testTag(BaseDialogTestTags.TEXT), text = type.message, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 2219e2642d..3ec5ffc54c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -106,7 +106,7 @@ fun AmountTextField( visualTransformation = visualTransformation, modifier = Modifier .background(backgroundColor) - .testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD), + .testTag(SendScreenTestTags.INPUT_TEXT_FIELD), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index 259553da0b..759d32590b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.CenterEnd import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +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 @@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SendAddressScreenTestTags import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION import kotlinx.coroutines.delay @@ -92,6 +94,7 @@ fun InputRowRecipient( text = it.resolveReference(), style = TangemTheme.typography.subtitle2, color = color, + modifier = Modifier.testTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE), ) } Box( @@ -115,7 +118,8 @@ fun InputRowRecipient( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) .weight(1f) - .align(CenterVertically), + .align(CenterVertically) + .testTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD), ) CrossIcon( onClick = onPasteClick, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseAmountBlockTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseAmountBlockTestTags.kt new file mode 100644 index 0000000000..f2f436d707 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseAmountBlockTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object BaseAmountBlockTestTags { + const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" + const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt new file mode 100644 index 0000000000..9ed0a4fbd6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseDialogTestTags { + const val CONTAINER = "BASE_DIALOG_CONTAINER" + const val TITLE = "BASE_DIALOG_TITLE" + const val TEXT = "BASE_DIALOG_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt deleted file mode 100644 index 0c5eae6e75..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.core.ui.test - -object DialogTestTags { - const val DIALOG_CONTAINER = "DIALOG_CONTAINER" -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SendAddressScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SendAddressScreenTestTags.kt new file mode 100644 index 0000000000..ebe05bc895 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SendAddressScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SendAddressScreenTestTags { + const val ADDRESS_TEXT_FIELD_TITLE = "SEND_ADDRESS_TEXT_FIELD_TITLE" + const val ADDRESS_TEXT_FIELD = "SEND_ADDRESS_TEXT_FIELD" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt new file mode 100644 index 0000000000..bc025954ac --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object SendScreenTestTags { + const val SCREEN_CONTAINER = "SEND_SCREEN_CONTAINER" + + const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE" + const val AMOUNT_CONTAINER_TEXT = "SEND_SCREEN_AMOUNT_CONTAINER_TEXT" + const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD" + const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT" + + const val CURRENCY_BUTTON = "SEND_SCREEN_CURRENCY_BUTTON" + const val FIAT_ICON = "SEND_SCREEN_FIAT_ICON" + const val CURRENCY_ICON = "SEND_SCREEN_CURRENCY_ICON" + const val MAX_BUTTON = "END_SCREEN_MAX_BUTTON" + const val PREVIOUS_BUTTON = "SEND_SCREEN_PREVIOUS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt index 138b27e35b..22cbf9c8a5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt @@ -1,10 +1,6 @@ package com.tangem.core.ui.test object StakingSendDetailsScreenTestTags { - - const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" - const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" - - const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" - const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" + const val VALIDATOR_BLOCK = "STAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" + const val NETWORK_FEE_BLOCK = "STAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt deleted file mode 100644 index 1dc36d3f79..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.core.ui.test - -object StakingSendScreenTestTags { - const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER" - - const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE" - const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT" - const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD" - const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT" - - const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON" - const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON" - const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON" - const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON" - const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON" -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index e1a7c81bd2..5170756b32 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -21,7 +21,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep @@ -47,7 +47,7 @@ internal fun StakingScreen(uiState: StakingUiState) { .fillMaxSize() .imePadding() .systemBarsPadding() - .testTag(StakingSendScreenTestTags.SCREEN_CONTAINER), + .testTag(SendScreenTestTags.SCREEN_CONTAINER), horizontalAlignment = Alignment.CenterHorizontally, ) { StakingAppBar( diff --git a/tangem-android-tools b/tangem-android-tools index 794a8187e6..4c0c522992 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a +Subproject commit 4c0c522992e9ae84c42eeb45564a56b84fe337ef From 155e7e3d2c3f25d2844e4ad14e8ea09eb9a490e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 16:22:54 +0300 Subject: [PATCH 63/69] Updated on 2026-08-14 --- .../di/UserWalletsListManagerModule.kt | 3 --- .../DefaultUserWalletsListRepository.kt | 18 +++++------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f582dea772..c521709443 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -8,7 +8,6 @@ import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -123,7 +122,6 @@ internal object UserWalletsListManagerModule { passwordRequester: HotWalletPasswordRequester, appPreferencesStore: AppPreferencesStore, hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, - scanCardProcessor: ScanCardProcessor, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -170,7 +168,6 @@ internal object UserWalletsListManagerModule { appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, - scanCardProcessor = scanCardProcessor, ) } 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 c6204135b7..fd30adec16 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 @@ -11,22 +11,16 @@ import com.tangem.common.map import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault -import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod +import com.tangem.domain.core.wallets.error.* 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.wallets.R import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.hot.HotWalletPasswordRequester -import com.tangem.domain.core.wallets.error.DeleteWalletError -import com.tangem.domain.core.wallets.error.LockWalletsError -import com.tangem.domain.core.wallets.error.SaveWalletError -import com.tangem.domain.core.wallets.error.SelectWalletError -import com.tangem.domain.core.wallets.error.SetLockError -import com.tangem.domain.core.wallets.error.UnlockWalletError -import com.tangem.domain.core.wallets.UserWalletsListRepository -import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.hot.sdk.model.HotWalletId import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey @@ -39,7 +33,6 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import com.tangem.core.analytics.models.AnalyticsParam import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -54,7 +47,6 @@ internal class DefaultUserWalletsListRepository( private val savePersistentInformation: ProviderSuspend, private val appPreferencesStore: AppPreferencesStore, private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, - private val scanCardProcessor: ScanCardProcessor, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -270,7 +262,7 @@ internal class DefaultUserWalletsListRepository( raise(UnlockWalletError.UnableToUnlock) } - scanCardProcessor.scan(analyticsSource = AnalyticsParam.ScreensSources.SignIn) + tangemSdkManagerProvider().scanProduct() .doOnSuccess { scanResponse -> val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build() From e0a5bf6e9e79b80fdb37a8cb12d26d9c986852ff Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Sep 2025 11:17:04 +0400 Subject: [PATCH 64/69] Updated on 2026-08-14 --- data/visa/build.gradle.kts | 1 + .../tangem/data/pay/di/TangemPayDataModule.kt | 8 ++++- .../pay/repository/DefaultKycRepository.kt | 10 ++---- .../pay/usecase/DefaultKycStartInfoUseCase.kt | 36 +++++++++++++++++++ .../domain/pay/repository/KycRepository.kt | 4 --- .../domain/pay/usecase/KycStartInfoUseCase.kt | 10 ++++++ .../com/tangem/features/kyc/KycComponent.kt | 7 +--- .../features/kyc/DefaultKycComponent.kt | 4 +-- .../tangem/features/kyc/DefaultKycModel.kt | 11 +++--- 9 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultKycStartInfoUseCase.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/KycStartInfoUseCase.kt diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 38d1d2957a..05adcdc660 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) /** Project - Utils */ implementation(projects.core.utils) 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 727e31f796..aab7f3c69d 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,7 +1,9 @@ package com.tangem.data.pay.di import com.tangem.data.pay.repository.DefaultKycRepository +import com.tangem.data.pay.usecase.DefaultKycStartInfoUseCase import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.pay.usecase.KycStartInfoUseCase import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,5 +16,9 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindKycRepositoryFactory(factory: DefaultKycRepository.Factory): KycRepository.Factory + fun bindKycRepository(repository: DefaultKycRepository): KycRepository + + @Binds + @Singleton + fun bindKycStartInfoUseCase(useCase: DefaultKycStartInfoUseCase): KycStartInfoUseCase } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt index 5ecfd916c5..20a9056985 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt @@ -13,10 +13,9 @@ 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 dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import javax.inject.Inject -class DefaultKycRepository @AssistedInject constructor( +class DefaultKycRepository @Inject constructor( @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, private val authDataSource: TangemPayAuthDataSource, @@ -65,9 +64,4 @@ class DefaultKycRepository @AssistedInject constructor( return Either.Left(VisaApiError.UnknownWithoutCode) } } - - @AssistedFactory - interface Factory : KycRepository.Factory { - override fun create(): DefaultKycRepository - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultKycStartInfoUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultKycStartInfoUseCase.kt new file mode 100644 index 0000000000..1ee376e005 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultKycStartInfoUseCase.kt @@ -0,0 +1,36 @@ +package com.tangem.data.pay.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.core.error.UniversalError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.pay.usecase.KycStartInfoUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.visa.error.VisaApiError +import javax.inject.Inject + +/** + * For TangemPay Customer Wallet auth we are using polygon address + */ +private const val POL_VALUE = "coin⟨POLYGON⟩polygon-ecosystem-token" + +internal class DefaultKycStartInfoUseCase @Inject constructor( + private val userWalletsRepository: UserWalletsListRepository, + private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase, + private val kycRepository: KycRepository, +) : KycStartInfoUseCase { + + override suspend fun invoke(): Either = either { + val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold + ?: raise(VisaApiError.UnknownWithoutCode) + + val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE)) + .getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode) + + kycRepository.getKycStartInfo(address, wallet.cardId).bind() + } +} \ 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 5d4ef99fd3..675923877b 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 @@ -15,8 +15,4 @@ interface KycRepository { * 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 - } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/KycStartInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/KycStartInfoUseCase.kt new file mode 100644 index 0000000000..8736279c57 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/KycStartInfoUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.pay.KycStartInfo + +interface KycStartInfoUseCase { + + suspend operator fun invoke(): Either +} \ No newline at end of file diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt index de05f5dc62..6a18888000 100644 --- a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -4,14 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext interface KycComponent { - fun launch(params: Params) + fun launch() interface Factory { fun create(appComponentContext: AppComponentContext): KycComponent } - - data class Params( - val targetAddress: String, - val cardId: String, - ) } \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index 92d39cee70..596b46c3ac 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -18,7 +18,7 @@ class DefaultKycComponent @AssistedInject constructor( private val model: DefaultKycModel = getOrCreateModel() - override fun launch(params: KycComponent.Params) { + override fun launch() { componentScope.launch { model.uiState.collect { it?.let { startInfo -> @@ -35,7 +35,7 @@ class DefaultKycComponent @AssistedInject constructor( } } } - model.getKycToken(params) + model.getKycToken() } @AssistedFactory diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt index aff81ad334..c46ad85577 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.pay.KycStartInfo -import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.pay.usecase.KycStartInfoUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,18 +15,15 @@ import javax.inject.Inject @ModelScoped class DefaultKycModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - kycRepositoryFactory: KycRepository.Factory, + private val kycStartInfoUseCase: KycStartInfoUseCase, ) : Model() { - private val kycRepository = kycRepositoryFactory.create() - private val _uiState: MutableStateFlow = MutableStateFlow(null) val uiState = _uiState.asStateFlow() - fun getKycToken(params: KycComponent.Params) { + fun getKycToken() { modelScope.launch { - kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull() - ?.let { _uiState.emit(it) } + kycStartInfoUseCase().getOrNull()?.let { _uiState.emit(it) } } } } \ No newline at end of file From 6e514da61077b1236a20156868baa71aef302b0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 10:45:42 +0200 Subject: [PATCH 65/69] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 3 +- core/res/src/main/res/values-es/strings.xml | 40 ++++---- core/res/src/main/res/values-fr/strings.xml | 3 +- core/res/src/main/res/values-ja/strings.xml | 92 ++++++++++++++++++- core/res/src/main/res/values-ru/strings.xml | 35 ++++++- .../src/main/res/values-uk-rUA/strings.xml | 22 ++++- core/res/src/main/res/values/strings.xml | 25 +++-- .../onramp/analytics/OnrampAnalyticsEvent.kt | 31 +++++++ .../alloffers/entity/AllOffersIntents.kt | 5 +- .../alloffers/entity/AllOffersStateFactory.kt | 7 +- .../onramp/alloffers/model/AllOffersModel.kt | 14 ++- .../onramp/main/entity/OnrampIntents.kt | 1 - .../main/model/OnrampMainComponentModel.kt | 2 - .../mainv2/entity/OnrampOfferBlockUM.kt | 23 ++++- .../onramp/mainv2/entity/OnrampV2Intents.kt | 13 +++ .../OnrampV2AmountFieldChangeConverter.kt | 3 +- .../factory/OnrampOffersStateFactory.kt | 4 +- .../factory/OnrampV2AmountStateFactory.kt | 3 +- .../entity/factory/OnrampV2StateFactory.kt | 3 +- .../model/OnrampV2MainComponentModel.kt | 14 +-- 20 files changed, 274 insertions(+), 69 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index b7cc837f73..a9bc17a583 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -984,7 +984,6 @@ Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar Sie senden - Von Von %s Grenzwert Gasgebühr Dies ist die maximale Gasgebühr, die für den Abschluss einer Transaktion oder eines Vertrags ausgegeben wird. Ein Gaslimit verhindert unerwartete oder unbegrenzte Gebühren bei der Ausführung einer Transaktion. @@ -1141,7 +1140,7 @@ Um mit dem Staking bei TON zu beginnen, führe zunächst eine ausgehende Transaktion in beliebiger Höhe durch – dadurch wird Deine Wallet aktiviert. Für den Abschluss der Transaktion können zusätzlich zur Netzwerkgebühr bis zu 0,2 TON erforderlich sein. Nicht genutzte Beträge werden zurückerstattet. Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 TON erforderlich. Bitte lade Dein Guthaben auf. - 0,2 Ton reserviert + TON-Reserve erforderlich Durch diese Aktion werden andere Positionen geschlossen oder gemäß den Netzwerkregeln in den Auszahlungsstatus versetzt. Positionsstatus Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 16cac3a4f8..31e91c7d59 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -196,7 +196,7 @@ Sincronizar direcciones Ir al proveedor Ir al token - Entiendo + Entendido Ocultar hora Importe @@ -880,8 +880,7 @@ Memo Compruebe su conexión de red Información de tarifa de red no accesible - Envía - Desde + Usted envía De %s Límite de gas Este es el máximo de gas que se gastará para completar una transacción o contrato. Un límite de gas evita cargos inesperados o ilimitados al ejecutar una transacción. @@ -895,7 +894,7 @@ Cobertura de tarifa de red Nonce Número único para cada transacción. Utilícelo para reenviar o cancelar una transacción pendiente. - Introduzca nonce.. + Introduzca nonce... Fondos insuficientes para la transferencia, ya que el total de la tarifa y el importe de la transferencia supera el saldo existente El total supera el saldo Se requiere un saldo de al menos %s para mantener su cuenta en blockchain para evitar riesgos de seguridad. Este monto permanecerá en su saldo y no podrá retirarse. @@ -950,6 +949,8 @@ Cantidad no válida La tarifa excede el saldo La cantidad total excede el saldo + ¿Está seguro que desea cambiar el token a recibir? Esto restablecerá los datos introducidos previamente. + Cambio de token Intercambiar y enviar ¿Continuar con la conversión? Esto borrará sus datos anteriores. Confirmar Conversión @@ -961,6 +962,7 @@ Cantidad a recibir El destinatario recibe %s ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. + Eliminar conversión Enviar con swap Transacción enviada Escanee la tarjeta/anillo que quiere configurar @@ -968,10 +970,10 @@ Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre Haga que su token trabaje para Ud. - El monto del staking debe ser al menos %s - El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. + La cantidad del staking debe ser al menos %s + La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red. El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red. - Unstaking de la reclamación + Reclamar lo que no está en staking Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. Porcentaje de rendimiento anual @@ -1007,7 +1009,7 @@ No hay validadores activos disponibles para hacer staking en este momento. Por favor, inténtalo de nuevo más tarde. Al hacer staking en la red Cardano, se utiliza todo su saldo. Se reservarán 2 ADA adicionales y se devolverán después del staking. Su ADA permanece desbloqueado mientras hace staking. Detalles del staking de ADA - Recomp. won se le enviará y estará disponible para su uso inmediato + Las recompensas obtenidas se enviarán a su billetera y estarán disponibles para su uso inmediato Haga staking de forma segura y comience a ganar recompensas Tangem permite a los usuarios hacer staking de sus criptomonedas Tangem permite a los usuarios hacer staking de sus criptomonedas @@ -1034,7 +1036,7 @@ Para empezar a hacer staking en TON, realice primero una transacción de salida de cualquier importe - esto activará su billetera. Es posible que se requieran hasta 0.2 TON además de la tarifa de red para completar la transacción. Cualquier cantidad no utilizada será reembolsada. Se requieren 0.2 TON para realizar esta operación, además de la tarifa de red. Por favor, recargue su saldo. - 0.2 Ton Reservado + Se requiere reserva de TON Esta acción cerrará otras posiciones o las cambiará al estado de retiro, de acuerdo con las reglas de la red. Estado de las posiciones Desbloquee su dinero para retirarlo del proceso de staking. El desbloqueo demora %s minuto. @@ -1044,9 +1046,9 @@ Hacer staking en la red Tron con un nuevo validador transferirá automáticamente todos los fondos de staking a este validador. Preparando Listo para retirar - Reunir - Haga el staking de nuevo - Restaking de las recompensas + Volver a unir + Hacer staking de nuevo + Hacer restaking de las recompensas Revocar Revotar Auto @@ -1080,7 +1082,7 @@ La cantidad para el unstaking debe ser al menos %s El monto excede el saldo apostado Sin staking - Unstaking + Deshacer el staking Detenando el staking de activos %s Validador Validadores @@ -1195,7 +1197,7 @@ Tangem Twin Esta acción es irreversible. No tendrá acceso la billetera antigua. Toque la tarjeta gemela con el número %s y no la retira hasta el final de la operación - Por favor, inténtelo de nuevo más tarde. Si el problema persiste, póngase en contacto con el servicio de asistencia. + Por favor, inténtelo de nuevo más tarde. Si el problema persiste, póngase en contacto con el servicio de soporte. ¡Algo salió mal! Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte. Use %s o escanee una tarjeta/anillo para tener acceso a su billetera @@ -1334,10 +1336,10 @@ Su opinión nos motiva a hacer Tangem Wallet aún mejor ¿Disfrutando de Tangem? Deba asociar su token antes de recibir tokens - Debe abrir una trustline para su token antes de recibirlo + Debe abrir una trustline (línea de confianza) para su token antes de recibirlo Se requiere tarifa de alquiler de red Acción requerida - ¿Se puso en contacto con el servicio de soporte a través de la app en los 7 días posteriores a la creación de la billetera? Si lo hiciste o no estás seguro, pulse en \"Sí\" y complete las instrucciones. + ¿Se puso en contacto con el servicio de soporte a través de la app en los 7 días posteriores a la creación de la billetera? Si lo hizo o no está seguro, pulse en \"Sí\" y complete las instrucciones. ¡Gracias! ¡Todo listo! No se requieren más acciones. Ahora será redirigido al sitio web oficial de Tangem. Por favor, lea y siga las instrucciones allí indicadas. ¿Alguna vez contactó al equipo de soporte de Tangem directamente desde esta aplicación? @@ -1355,11 +1357,11 @@ Solo para fines de prueba El saldo podría estar desactualizado. Actualice la página. No hay suficiente %1$s. Recargue su cuenta %2$s para asociar este token - Habilitar Trustline + Habilitar Trustline (línea de confianza) Una trustline (línea de confianza) debe estar habilitada para recibir este token. La red requiere un %1$s %2$s reserva. Se requiere trustline (línea de confianza) - La red requerida %s no está añadida a su portafolio. Añádala primero y luego continúe con la conexión. - Agregue red al portafolio + La red requerida %s no está agregada a su portafolio. Agréguela primero y luego continúe con la conexión. + Agregar red al portafolio Dominio malicioso Dominio desconocido Conectarse de todas formas diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 0d2ee7b831..2b20ddae59 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -854,7 +854,6 @@ Vérifiez votre connexion réseau Informations sur les frais de réseau inaccessibles Vous envoyez - De De %s Limite de gaz Il s\'agit du montant maximal de gaz qui sera dépensé pour effectuer une transaction ou un contrat. Une limite de gaz empêche des frais imprévus ou illimités lors de l\'exécution d\'une transaction. @@ -1011,7 +1010,7 @@ Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. 0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. - 0,2 Ton réservés + Réserve de TON requise Cette action fermera d\'autres positions ou les fera passer au statut de retrait, conformément aux règles du réseau. Statut des positions Débloquez votre argent pour le retirer du processus de staking. Le déverrouillage prend %s. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ff93a67733..5205787420 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -35,6 +35,7 @@ アカウントを追加 アカウントを編集 %1$s ( %2$s内) + メインアカウント アカウントが回復しました アカウントを長押しして並べ替える 編集を続ける @@ -179,6 +180,7 @@ 許可する 金額 アナリティクス + そして 適用する 承認 承認 @@ -242,6 +244,7 @@ 終了 無料 アドレスを同期する + はじめる プロバイダーへ移動 トークンへ移動 わかりました @@ -250,6 +253,7 @@ インポート 進行中 後で + もっと詳しく 残り%1$s レガシービットコイン ロックされています @@ -387,6 +391,7 @@ デフォルトアドレス レガシーアドレス 資金を受け取る + %sアドレス 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する @@ -410,7 +415,7 @@ %sによる交換 返金を受けるには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる操作が失敗しました。 - スワップに予想以上の時間がかかっています。プロバイダーサポートにお問い合わせください。 + スワップは通常より時間がかかっておりますが、資金は完全に安全であり、送金されます。ご不明な点がございましたら、プロバイダーのサポートチームまでお問い合わせください。 長い取引時間 OKXまたはブリッジのルールにより、取引金額は%1$sでウォレットに返金されました。%2$s 金額は %1$s(%2$sネットワーク)で返金されました @@ -495,6 +500,7 @@ 新しいウォレットを作成する Tangemを注文 Tangemをスキャン + 「Tangem」に生体認証の使用を許可しますか?\n本人確認とアプリの起動のために使用されます。 %sへ %sネットワーク アクセスコードの作成プロセスを終了してもよろしいですか? @@ -786,6 +792,7 @@ 追加しようとしているカードには、すでに別のウォレットが作成されています。このウォレットに資金がある場合は、それを引き出してからこのカードをリセットし、バックアップとして追加してください。 ウォレットを保存する バックアップの作成 + 生体認証を使用する シードフレーズについてもっと読む これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。 @@ -837,6 +844,7 @@ アクセスコードの復元 同一のカード アクセスコード + すべてのオファー %sで利用可能 プロバイダーが取引を促進します 国で検索 @@ -844,13 +852,22 @@ その他の通貨 人気の法定通貨 通貨で検索 + 即時 オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします 買付金額は%s以下にしてください 買付金額は少なくとも%sである必要があります この通貨で利用可能なプロバイダーはありません + 最速 支払う + 支払方法 最大 %s まで使用可能 %s 以上で利用可能 + + %dプロバイダー + + プロバイダー + 最近使用した + 推奨 サードパーティプロバイダー%sで取引を完了できます。 %sにリダイレクトしています... この国では当社のサービスはご利用いただけません @@ -859,7 +876,13 @@ 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 設定 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 + + 最大%d日 + + %s分 + 以下が手に入ります。 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 + 最大 経由 支払い グループ @@ -970,7 +993,6 @@ ネットワーク接続を確認してください ネットワーク手数料についての情報にアクセスできません 送信する - より %sから ガス上限 これは、取引または契約を完了するために使用されるガスの最大額です。ガス上限を設定することで、取引実行時に予期せぬ請求や無制限の請求を防ぐことができます。 @@ -1014,7 +1036,7 @@ 有効なアドレスではありません トークンの損失を避けるために、受信ウォレットアドレスが** %sネットワーク**上にあることを確認してください %s に送信 - %sネットワークアドレスを確認してください。エラーが発生すると転送が失われる可能性があります。 + %sネットワーク上のアドレスに送信していることを確認してください。エラーが発生すると資金が失われる可能性があります。 送金先 メモ/宛先タグは、同じネットワーク上の同じ受信者に送信された取引を区別するためのユニークなIDです。注意:メモを省略すると、資金の行き違いにつながる可能性があります。 メモ / 宛先タグは、暗号資産ネットワーク内の共有受信者への取引を分離するコードです。 @@ -1025,7 +1047,7 @@ 送金中... 変更するには任意の箇所をタップしてください %sを送金する - **%1$s** を送金する (ネットワーク手数料%2$sを含む) + %2$sのネットワーク料を含む**%1$s**を送信しています。 **%1$s** と %2$s を送金しています。 ** %1$s ** を送信しています ネットワーク手数料は%1$sエネルギーを使用してカバーされます @@ -1127,7 +1149,7 @@ TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。 取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。 この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 - 0.2 TON保留済み + TONの準備金が必要です このアクションは、ネットワークのルールに従って、他のポジションをクローズするか、引き出しステータスに切り替えます。 ポジション状況 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 @@ -1601,4 +1623,64 @@ いいえ、すべて送信します %s XTZを減らす 次回ウォレットにチャージするときに手数料の増加を避けるには、金額を%s XTZ減らしてください。 + 承認する + 前回の承認に問題があったため、新しい承認が必要です。手続き方法を選択してください。 + 承認が必要 + 手数料が差し引かれ、あなたの資産は再び貸し出されます。 + 引き続き収益を得るには承認が必要です。 + 承認を確定する + 資産残高 + あなたの%sはAaveに預けられています + %s%を獲得 + 利用可能 + 現在のAPY + 私の資金 + あなたの%1$sはAaveに預けられ、利息が付きます。あなたは%2$sトークンを保有しており、これは残高を表し、時間の経過とともに増加します。入金すると、資金はAaveに預け入れられ、取引手数料を差し引いた利息が付きます。 + アクティブ + 利回りを得る + 総収益 + Aaveへの送金 + 資産はプロトコル内に保管されています + Aaveの詳細を見る + あなたの%sはAaveに預けられています + これは%sの現在の供給手数料です。実際のコストは受取画面に表示されます。 + 現在の手数料 + 今後の%sの入金はすべて、取引手数料が差し引かれて自動的にAaveに供給されます。 + ネットワーク手数料が上限手数料を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。 + 最大手数料 + 手数料ポリシー + ここに説明文を入力してください。1〜3行が理想的です。 + トークン承認が必要 + アカウントへの入金はすべて自動的にAaveに貸し出されます。 + 残高は自動的に計算されます + いつでも、即座に資金を送信、交換、売却できます。 + すぐに現金化 + 使い方 + Aaveは世界中で何百万人もの人々に信頼されています。総貸付額は104億ドルです。 + 分散型・自己管理型 + サービスを使用することにより、プロバイダーに同意したものとみなされます + 年間%s%の収益 + Aave • 変動金利 + Aave + 平均%s + 昨年のリターン + 現在の金利は常に変動し、リアルタイムの需要と供給に基づいて、AAVEオンチェーンスマートコントラクトによって自動的に計算されます。 + 提供 + 金利は変動します + 入金すると、資金は自動的にAaveに送金され、利息が付き始めます。取引手数料として%s相当の少額の手数料が差し引かれます。 + 利息の獲得を開始 + %sはAaveに供給され、すぐに利用可能になります + 手数料ポリシーを見る + 次回の入金は自動的にAaveに供給されます。 + 収益を停止する + オフにすると、Aaveから資金が引き出され、ウォレットの%sに戻され、報酬の獲得が停止されます。 + 出金金額からネットワーク手数料が差し引かれます。 + APY + あなたの資産を眠らせない — 残高を運用して利息を得ましょう。 + 保有資産から報酬を得る + 入金の処理中 + 年間%1$s%%の収益 + 自動 + 取引のネットワーク手数料をカバーするために、 %1$s %2$sを入金してください + %s手数料を支払えません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 34e2073bb1..58186b2510 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -142,6 +142,7 @@ Подтвердить Подключение Продолжить + Конвертировать Копировать Скопировать адрес Создать @@ -236,6 +237,7 @@ Отправить Успешно Поддержка + Поддерживаемые сети Обменять условия участия Условиями использования @@ -743,6 +745,7 @@ Восстановление кода доступа Идентичные карты Код доступа + Все предложения Доступно с %s Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами Поиск по стране @@ -750,13 +753,25 @@ Другие валюты Популярные фиаты Поиск по валюте + Моментально Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s Сумма покупки не может быть больше, чем %s Сумма покупки должна составлять минимум %s Нет доступных провайдеров для выбранной валюты + Самый быстрый Оплата с + Платежный метод Доступно до %s Доступно от %s + + %d провайдер + %d провайдера + %d провайдеров + %d провайдеров + + Провайдеры + Ранее используемый + Рекомендуемый Вы сможете продолжить транзакцию через сервис стороннего провайдера, %s Переход к %s... Наши сервисы недоступны в данной стране @@ -765,8 +780,18 @@ Пожалуйста, укажите правильную страну, чтобы обеспечить точность доступных способов оплаты и услуг. Настройки You can close this screen and check the transaction status on the token details screen. + + до %d дня + до %d дней + до %d дней + до %d дней + + %s мин + Вы получите Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене. + До Через + Вы заплатите Группы По балансу Упорядочить токены @@ -854,6 +879,7 @@ Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена + Конвертировать в другой токен Сумма Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte. Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. @@ -879,7 +905,7 @@ Проверьте своё интернет соединение Информация о комиссии сети недоступна Вы отправляете - Из + Из %s Лимит газа Это максимальное количество газа, которое будет потрачено на выполнение транзакции или контракта. Лимит газа предотвращает неожиданные или неограниченные расходы при выполнении транзакции. Цена газа @@ -920,6 +946,7 @@ Получатель Неверный адрес Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов. + Убедитесь, что отправляете на адрес в сети %s, ошибки могут привести к потере средств. Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Memo / Destination Tag — это код, разделяющий транзакции, отправляемые одному и тому же получателю в криптовалютной сети. @@ -948,10 +975,13 @@ Обмен и отправка Продолжить с обменом? Это действие удалит предыдущие данные Подтвердить конвертацию + Отправка любой другой валюты приведёт к её безвозвратной потере. + Выберите правильную сеть получателя Выберите любой токен к получению. Ваш адресат получит ровно то, что вы выбрали — без лишних сложностей. Будет получено Получателю Сумма к получению + К получению %s Вы уверены, что хотите отменить конвертацию? Ваши предыдущие данные будут удалены. Убрать конвертацию Отправка с обменом @@ -1026,7 +1056,7 @@ Чтобы начать стейкинг в TON, сначала осуществите исходящую транзакцию на любую сумму — это активирует ваш кошелек. До 0.2 TON может потребоваться сверх сетевой комиссии для завершения транзакции. Неиспользованная часть будет возвращена. Для выполнения операции требуется дополнительно 0.2 TON, помимо сетевой комиссии. Пожалуйста, пополните баланс. - Зарезервировано 0.2 TON + Требуется резерв TON Это действие закроет другие позиции или переведёт их в статус вывода средств в соответствии с правилами сети. Статус позиций Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s. @@ -1187,6 +1217,7 @@ Tangem Twin Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции + Пожалуйста, попробуйте позже. Если проблема сохранится, обратитесь в поддержку. Произошла ошибка. Код ошибки: %s. Попробуйте, пожалуйста, снова. Если проблема будет продолжать возникать — обратитесь в службу поддержки. Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения. 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 33db0a745b..8ef3fe2fc2 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -134,6 +134,7 @@ Підтвердити Підключення Продовжити + Конвертувати Копіювати Скопіювати адресу Створити @@ -836,6 +837,7 @@ Ви вказали комісію нижче рекомендованої, це може спричинити затримку вашої транзакції. Продовжити? Причина: %1$s\nКод: %2$s Транзакція не завершена + Конвертувати в інший токен Сума Ви можете встановити розмір комісії за транзакцію, налаштувавши значення в полі Satoshi per vByte. Комісія, яка буде стягнута за вашу транзакцію. Ви можете встановити власне значення. @@ -861,7 +863,6 @@ Перевірте підключення до мережі Інформація щодо комісії в мережі недоступна Ви надсилаєте - Із Ліміт газу Це максимальна кількість газу, яка буде витрачена на здійснення транзакції чи контракту. Ліміт газу запобігає несподіваним або необмеженим стягненням під час виконання транзакції. Вартість газу @@ -902,6 +903,7 @@ Одержувач Недійсна адреса Переконайтеся, що адреса гаманця одержувача знаходиться в мережі %s, щоб уникнути втрати ваших токенів + Переконайтеся, що ви надсилаєте на адресу в мережі %s. Помилка може призвести до втрати коштів. Надіслати до Memo/ Тег призначення — це унікальний ідентифікатор для розрізнення транзакцій, надісланих тому самому одержувачу в тій самій мережі. Застереження: відсутність тегу може призвести до втрати коштів. Мемо / Destination Tag — це код, що розділяє транзакції до спільного одержувача в криптовалютній мережі. @@ -926,7 +928,21 @@ Недопустима сума Комісія перевищує залишок Сума, що відправляється, перевищує залишок + Ви впевнені, що хочете змінити токен отримання? Це призведе до скидання раніше введених даних. + Зміна токену + Обмін та надсилання + Продовжити обмін? Це очистить ваші попередні дані. + Підтвердити конвертацію + Надсилання будь-якої іншої валюти призведе до її незворотної втрати. + Виберіть правильну мережу одержувача Виберіть будь-який токен. Той, кому ви його надішлете, отримає саме те, що ви обрали — без жодних ускладнень. + Буде отримано + Отримувачу + Сума для отримання + До отримання %s + Ви впевнені, що хочете скасувати конвертацію? Ваші попередні дані будуть видалені. + Прибрати конвертацію + Надіслати з обміном Трансакцію надіслано Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати. Забути гаманець @@ -999,7 +1015,7 @@ Щоб почати стейкінг в TON, спочатку здійсніть вихідну транзакцію на будь-яку суму — це активує ваш гаманець. До 0.2 TON може знадобитися додатково мережевої комісії для завершення транзакції. Невикористана частина буде повернута. Для виконання операції потребується додатково 0.2 TON, крім мережевої комісії. Будь ласка, поповніть баланс. - Зарезервовано 0.2 TON + Потрібен резерв TON Ця дія закриє інші позиції або переведе їх у статус виведення, відповідно до правил мережі. Статус позицій Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s. @@ -1159,6 +1175,7 @@ Tangem Twin Ця дія є незворотною. Ви не матимете доступу до старого гаманця. Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції + Спробуйте ще раз пізніше. Якщо проблема залишиться, зверніться до служби підтримки. Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. @@ -1383,6 +1400,7 @@ Відомий ризик безпеки Відкрийте програму Web3 та виберіть опцію WalletConnect Запит від + Підписати все одно Тип підпису Для підключення dApp потрібна принаймні одна мережа Вкажіть вибрані мережі diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c3d240ace5..1faf5263e0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -249,6 +249,7 @@ Finish Free Synchronize addresses + Get started Go to provider Go to token Got it @@ -1055,7 +1056,7 @@ Not a valid address Ensure the receiving wallet address is on **the %s network** to avoid losing your tokens send to %s - Ensure you %s network address, as errors may result in lost transfers + Make sure you are sending to an address on the %s network. An error may result in the loss of funds. Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds Memo / Destination Tag is a code separating transactions to a shared recipient in a crypto network. @@ -1092,7 +1093,7 @@ Recipient will receive To recipient Amount to receive - Recipient get %s + Recipient gets %s Are you sure you want to cancel the conversion? Your previous data will be cleared. Remove Conversion Send with swap @@ -1168,7 +1169,7 @@ To start staking in TON, first make an outgoing transaction of any amount — this will activate your wallet. Up to 0.2 TON may be required in addition to the network fee to complete the transaction. Any unused amount will be refunded. 0.2 TON is required to proceed with this operation, in addition to the network fee. Please top up your balance. - 0.2 Ton Reserved + TON reserve required This action will close other positions or switch them to withdrawal status, according to network rules. Positions status Unlock your money to withdraw it from staking process. Unlocking takes %s. @@ -1693,7 +1694,7 @@ To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ Give Approve Something went wrong with your previous approval, so we need a new one. Choose how you’d like to proceed. - Approve nedeed + Approve needed The fee will be taken out, and your assets will be lent again. To continue earning, approval is required. Confirm approval @@ -1704,16 +1705,12 @@ Current APY My Funds Your %1$s is now deposited in Aave and earning interest. You hold a%2$s token, which represents your balance and grows over time. When you top up, funds go to Aave to earn interest, minus a transaction fee. - Read more Active - Stop earning Earn Total earnings - Automatic Transfers to Aave Text explaining that your savings are located in the protocol Explore Aave - Explore default Your %s is deposited in Aave This is the current supply fee on %s. The live cost will be shown on the Receive Screen. Current fee @@ -1731,28 +1728,28 @@ Aave is trusted by millions worldwide. Total lended value is $10.4B. Decentralized and self-custodial By using service, you agree with provider - Maximize your savings + Earn %s% yearly Aave • Variable Interest Rate Aave Avg %s Last year returns Current interest rate is always variable and automatically computed by AAVE on-chain smart-contract based on real-time supply and demand. + Powered by Interest rate is variable When you top up, your funds will be automatically sent to Aave to start earning interest. A small fee equal to %s will be deducted to cover the transaction. - Start earning + Start earning Your %s will be supplied to Aave and will stay instantly available See fee policy - Network fee Your next deposits will be automatically supplied to Aave. - Start earning + Stop earning Turning off will withdraw your funds from Aave, return them to %s in your wallet, and stop earning rewards. The network fee will be deducted from the amount you withdraw. - Stop earning APY Make your money work — earn interest on your balance. Earning on your balance Processing your deposit - Earn %s% per year + Earn %1$s%% per year + Automatic Deposit some %1$s %2$s to cover the network fee for transactions Unable to cover %s fee diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index cf54baee5b..7dd1e1a772 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -163,4 +163,35 @@ sealed class OnrampAnalyticsEvent( ERROR_DESCRIPTION to errorDescription, ), ) + + data class FastestBuyMethodClicked( + private val tokenSymbol: String, + private val providerName: String, + private val paymentMethod: String, + ) : OnrampAnalyticsEvent( + event = "Fastest Method Clicked", + params = mapOf( + TOKEN_PARAM to tokenSymbol, + PROVIDER to providerName, + PAYMENT_METHOD to paymentMethod, + ), + ) + + data class BestRateClicked( + private val tokenSymbol: String, + private val providerName: String, + private val paymentMethod: String, + ) : OnrampAnalyticsEvent( + event = "Best Rate Clicked", + params = mapOf( + TOKEN_PARAM to tokenSymbol, + PROVIDER to providerName, + PAYMENT_METHOD to paymentMethod, + ), + ) + + data object AllOffersClicked : OnrampAnalyticsEvent( + event = "Button - All Offers", + params = emptyMap(), + ) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt index 4d2bbcec90..2f3739e450 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -1,12 +1,13 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -interface AllOffersIntents { +internal interface AllOffersIntents { fun onPaymentMethodClicked(paymentMethodId: String) - fun onBuyClick(quote: OnrampProviderWithQuote.Data) + fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) fun onBackClicked() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 67878d0738..351cd1a5aa 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -1,10 +1,12 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler 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.format.bigdecimal.percent +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM @@ -16,6 +18,7 @@ import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal internal class AllOffersStateFactory( + private val analyticsEventHandler: AnalyticsEventHandler, private val currentStateProvider: Provider, private val allOffersIntents: AllOffersIntents, ) { @@ -59,6 +62,7 @@ internal class AllOffersStateFactory( fun getPaymentsState(): AllOffersStateUM { return when (val currentState = currentStateProvider.invoke()) { is AllOffersStateUM.Content -> { + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) currentState.copy(currentMethod = null) } AllOffersStateUM.Loading, @@ -133,12 +137,13 @@ internal class AllOffersStateFactory( }, onBuyClicked = { allOffersIntents.onBuyClick( - OnrampProviderWithQuote.Data( + quote = OnrampProviderWithQuote.Data( provider = quote.provider, paymentMethod = quote.paymentMethod, toAmount = quote.toAmount, fromAmount = quote.fromAmount, ), + onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages), ) }, ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 19e69d6696..bd6674ad34 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -11,6 +11,7 @@ import com.tangem.features.onramp.alloffers.AllOffersComponent import com.tangem.features.onramp.alloffers.entity.AllOffersIntents import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM +import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.Job @@ -30,6 +31,7 @@ internal class AllOffersModel @Inject constructor( private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { AllOffersStateFactory( + analyticsEventHandler = analyticsEventHandler, currentStateProvider = Provider { state.value }, allOffersIntents = this, ) @@ -42,6 +44,8 @@ internal class AllOffersModel @Inject constructor( init { subscribeOnAllOffers() + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked) } fun dismiss() { @@ -51,10 +55,13 @@ internal class AllOffersModel @Inject constructor( override fun onPaymentMethodClicked(paymentMethodId: String) { val contentState = state.value as? AllOffersStateUM.Content ?: return val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return + analyticsEventHandler.send( + event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name), + ) _state.update { contentState.copy(currentMethod = method) } } - override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + override fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) { analyticsEventHandler.send( OnrampAnalyticsEvent.OnBuyClick( providerName = quote.provider.info.name, @@ -62,6 +69,11 @@ internal class AllOffersModel @Inject constructor( tokenSymbol = params.cryptoCurrency.symbol, ), ) + onrampOfferAdvantagesUM.toAnalyticsEvent( + cryptoCurrencySymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethodName = quote.paymentMethod.name, + )?.let { analyticsEventHandler.send(it) } params.openRedirectPage(quote) } 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 6ed094c0cb..5654a17174 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,5 +10,4 @@ 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 f78daf8e79..71399a3ce4 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 @@ -289,8 +289,6 @@ 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/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt index 5e41b49346..54ea1f1451 100644 --- 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 @@ -2,6 +2,7 @@ package com.tangem.features.onramp.mainv2.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.collections.immutable.ImmutableList @@ -43,7 +44,27 @@ internal enum class OnrampOfferCategoryUM { } internal enum class OnrampOfferAdvantagesUM { - Default, BestRate, Fastest + Default, BestRate, Fastest; + + fun toAnalyticsEvent( + cryptoCurrencySymbol: String, + providerName: String, + paymentMethodName: String, + ): OnrampAnalyticsEvent? { + return when (this) { + BestRate -> OnrampAnalyticsEvent.BestRateClicked( + tokenSymbol = cryptoCurrencySymbol, + providerName = providerName, + paymentMethod = paymentMethodName, + ) + Fastest -> OnrampAnalyticsEvent.FastestBuyMethodClicked( + tokenSymbol = cryptoCurrencySymbol, + providerName = providerName, + paymentMethod = paymentMethodName, + ) + Default -> null + } + } } internal data class OnrampAllOffersButtonConfig( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt new file mode 100644 index 0000000000..a9f65922cd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onramp.mainv2.entity + +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + +internal interface OnrampV2Intents { + fun onAmountValueChanged(value: String) + fun openSettings() + fun openCurrenciesList() + fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) + fun openProviders() + fun onRefresh() + fun onContinueClick() +} \ 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 index e0947e34b5..ec94be047d 100644 --- 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 @@ -8,7 +8,6 @@ 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 @@ -18,7 +17,7 @@ import java.math.BigDecimal internal class OnrampV2AmountFieldChangeConverter( private val currentStateProvider: Provider, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val onrampIntents: OnrampIntents, + private val onrampIntents: OnrampV2Intents, private val cryptoCurrency: CryptoCurrency, ) : Converter { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt index 377c5987d4..966893cab7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.onramp.model.* 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 com.tangem.utils.StringsSigns.MINUS @@ -15,7 +14,7 @@ import kotlinx.collections.immutable.toPersistentList internal class OnrampOffersStateFactory( private val currentStateProvider: Provider, - private val onrampIntents: OnrampIntents, + private val onrampIntents: OnrampV2Intents, ) { fun getOnShowOffersState(offers: List): OnrampV2MainComponentUM { @@ -65,6 +64,7 @@ internal class OnrampOffersStateFactory( toAmount = currentQuote.toAmount, fromAmount = currentQuote.fromAmount, ), + onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages), ) }, ), 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 index 3c7a477fd1..0e1dc10b5b 100644 --- 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 @@ -15,7 +15,6 @@ 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 @@ -24,7 +23,7 @@ import java.math.BigDecimal internal class OnrampV2AmountStateFactory( private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampIntents, + private val onrampIntents: OnrampV2Intents, private val cryptoCurrency: CryptoCurrency, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, ) { 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 index 8e37235b44..e39a2078b2 100644 --- 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 @@ -20,7 +20,6 @@ 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 @@ -29,7 +28,7 @@ internal class OnrampV2StateFactory( private val currentStateProvider: Provider, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, private val cryptoCurrency: CryptoCurrency, - private val onrampIntents: OnrampIntents, + private val onrampIntents: OnrampV2Intents, ) { fun getInitialState( 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 index abead667d7..57cdbcf6b6 100644 --- 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 @@ -15,7 +15,6 @@ 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.* @@ -48,7 +47,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( private val getOnrampOffersUseCase: GetOnrampOffersUseCase, paramsContainer: ParamsContainer, getWalletsUseCase: GetWalletsUseCase, -) : Model(), OnrampIntents { +) : Model(), OnrampV2Intents { val params = paramsContainer.require() @@ -126,7 +125,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) } - override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + override fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) { val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return analyticsEventHandler.send( OnrampAnalyticsEvent.OnBuyClick( @@ -135,6 +134,11 @@ internal class OnrampV2MainComponentModel @Inject constructor( tokenSymbol = params.cryptoCurrency.symbol, ), ) + onrampOfferAdvantagesUM.toAnalyticsEvent( + cryptoCurrencySymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethodName = quote.paymentMethod.name, + )?.let { analyticsEventHandler.send(it) } params.openRedirectPage(quote) } @@ -159,10 +163,6 @@ internal class OnrampV2MainComponentModel @Inject constructor( } } - 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) { From a8f084ae8835064f0dd6f16adcfda9e785195044 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 11:38:08 +0200 Subject: [PATCH 66/69] Updated on 2026-08-14 --- .../tangem/features/onramp/alloffers/model/AllOffersModel.kt | 2 +- .../features/onramp/mainv2/model/OnrampV2MainComponentModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index bd6674ad34..29414b13c0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -73,7 +73,7 @@ internal class AllOffersModel @Inject constructor( cryptoCurrencySymbol = params.cryptoCurrency.symbol, providerName = quote.provider.info.name, paymentMethodName = quote.paymentMethod.name, - )?.let { analyticsEventHandler.send(it) } + )?.let { analyticsEventHandler::send } params.openRedirectPage(quote) } 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 index 57cdbcf6b6..93f199fc85 100644 --- 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 @@ -138,7 +138,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( cryptoCurrencySymbol = params.cryptoCurrency.symbol, providerName = quote.provider.info.name, paymentMethodName = quote.paymentMethod.name, - )?.let { analyticsEventHandler.send(it) } + )?.let { analyticsEventHandler::send } params.openRedirectPage(quote) } From 4d5b9b87e6fbfb431e31589a600e9bf5d3ede78e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Sep 2025 17:12:51 +0400 Subject: [PATCH 67/69] Updated on 2026-08-14 --- .../token/DefaultUserTokensResponseStore.kt | 8 ++++ .../local/token/UserTokensResponseStore.kt | 2 + .../GetWalletAccountsResponseConverter.kt | 4 +- .../FetchWalletAccountsErrorHandler.kt | 7 ++-- .../DefaultAccountsCRUDRepository.kt | 5 +-- .../GetWalletAccountsResponseConverterTest.kt | 1 - .../FetchWalletAccountsErrorHandlerTest.kt | 2 +- .../DefaultAccountsCRUDRepositoryTest.kt | 6 +-- .../common/cache/etag/DefaultETagsStore.kt | 40 +++++++++++++++++++ .../data/common/cache/etag/ETagsStore.kt | 34 ++++++++++++++++ .../UserTokensResponseAccountIdEnricher.kt | 4 +- .../tangem/data/common/di/DataCommonModule.kt | 9 +++++ 12 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt index 0a80baa680..89f266e889 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt @@ -39,5 +39,13 @@ internal class DefaultUserTokensResponseStore( ) } + override suspend fun clear(userWalletId: UserWalletId) { + appPreferencesStore.updateData { preferences -> + val key = createPreferencesKey(userWalletId = userWalletId.stringValue) + + preferences.toMutablePreferences().apply { remove(key) } + } + } + private fun createPreferencesKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId") } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt index 9631d25c78..9da0c72837 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt @@ -17,4 +17,6 @@ interface UserTokensResponseStore { suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse) + + suspend fun clear(userWalletId: UserWalletId) } \ 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 index 4ae6bf8b41..3f741ba0b5 100644 --- 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 @@ -14,7 +14,6 @@ import dagger.assisted.AssistedInject */ internal class GetWalletAccountsResponseConverter @AssistedInject constructor( @Assisted private val userWallet: UserWallet, - @Assisted val version: Int, cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, ) : Converter { @@ -25,7 +24,6 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor( 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, @@ -39,6 +37,6 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet, version: Int): GetWalletAccountsResponseConverter + fun create(userWallet: UserWallet): GetWalletAccountsResponseConverter } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 7142520d72..520868e3f6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -104,9 +104,10 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( ): UserTokensResponse? { if (this != null) return this - return userTokensResponseStore.getSyncOrNull( - userWalletId = userWalletProvider().walletId, - ) + val userWalletId = userWalletProvider().walletId + + return userTokensResponseStore.getSyncOrNull(userWalletId) + .also { userTokensResponseStore.clear(userWalletId) } } private fun UserTokensResponse?.orDefault(userWalletProvider: Provider): UserTokensResponse { 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 3d148930d5..333001ed0e 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 @@ -112,10 +112,7 @@ internal class DefaultAccountsCRUDRepository( .getOrThrow() } - val converter = convertersContainer.getWalletAccountsResponseCF.create( - userWallet = accountList.userWallet, - version = version, - ) + val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet) val accountsResponse = converter.convert(value = accountList) 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 index b6dddb7aa5..766544b58a 100644 --- 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 @@ -26,7 +26,6 @@ class GetWalletAccountsResponseConverterTest { private val cryptoPortfolioConverter = mockk() private val converter = GetWalletAccountsResponseConverter( userWallet = userWallet, - version = 0, cryptoPortfolioConverterFactory = cryptoPortfolioConverterFactory, ) diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 5d41955371..532b33975b 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -30,7 +30,7 @@ class FetchWalletAccountsErrorHandlerTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val userWalletsStore: UserWalletsStore = mockk() - private val userTokensResponseStore: UserTokensResponseStore = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk() private val cryptoPortfolioConverter = mockk() private val userTokensResponseFactory: UserTokensResponseFactory = mockk() 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 index 0002eb553d..8fbcf92aea 100644 --- 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 @@ -595,7 +595,7 @@ class DefaultAccountsCRUDRepositoryTest { } every { - convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet, version = version) + convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter coEvery { accountsResponseStore.updateData(transform = any()) } returns accountsResponse @@ -610,7 +610,7 @@ class DefaultAccountsCRUDRepositoryTest { accountsResponseStoreFactory.create(userWalletId) accountsResponseStore.data tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body) - convertersContainer.getWalletAccountsResponseCF.create(userWallet, version) + convertersContainer.getWalletAccountsResponseCF.create(userWallet) converter.convert(accountList) accountsResponseStore.updateData(any()) } @@ -656,7 +656,7 @@ class DefaultAccountsCRUDRepositoryTest { } coVerify(inverse = true) { - convertersContainer.getWalletAccountsResponseCF.create(any(), any()) + convertersContainer.getWalletAccountsResponseCF.create(any()) accountsResponseStore.updateData(any()) } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt new file mode 100644 index 0000000000..5c8a9b2dc1 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt @@ -0,0 +1,40 @@ +package com.tangem.data.common.cache.etag + +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.getSyncOrNull +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.models.wallet.UserWalletId +import timber.log.Timber + +/** + * Default implementation of the [ETagsStore] interface for managing ETag values + * + * @property appPreferencesStore the preferences store used for saving and retrieving ETag values + */ +internal class DefaultETagsStore( + private val appPreferencesStore: AppPreferencesStore, +) : ETagsStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId, key: ETagsStore.Key): String? { + val key = getAccountsETagKey(userWalletId = userWalletId, key = key) + + return appPreferencesStore.getSyncOrNull(key = key) + } + + override suspend fun store(userWalletId: UserWalletId, key: ETagsStore.Key, value: String) { + if (value.isBlank()) { + Timber.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key") + return + } + + val key = getAccountsETagKey(userWalletId = userWalletId, key = key) + + appPreferencesStore.store(key = key, value = value) + } + + private fun getAccountsETagKey(userWalletId: UserWalletId, key: ETagsStore.Key): Preferences.Key { + return stringPreferencesKey(name = "etag_${key}_${userWalletId.stringValue}") + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt new file mode 100644 index 0000000000..430e9a0290 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt @@ -0,0 +1,34 @@ +package com.tangem.data.common.cache.etag + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Interface for working with ETag (Entity Tag), which is used for data caching and validation. + * +[REDACTED_AUTHOR] + */ +interface ETagsStore { + + /** + * Retrieves the stored ETag value for the specified wallet and key + * + * @param userWalletId identifier of the user wallet + * @param key the key for which to get the ETag value + */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, key: Key): String? + + /** + * Stores the ETag value for the specified wallet and key + * + * @param userWalletId identifier of the user wallet + * @param key the key for which to get the ETag value + */ + suspend fun store(userWalletId: UserWalletId, key: Key, value: String) + + /** Enumeration of possible keys for storing ETag values */ + enum class Key { + WalletAccounts, + UserTokens, + ; + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt index f91e9f40f3..3594f6f1a0 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt @@ -50,9 +50,9 @@ object UserTokensResponseAccountIdEnricher { if (enrichedTokens.isEmpty()) return tokens + val enrichedTokenMap = enrichedTokens.associateBy { it } return tokens.map { token -> - val enrichedToken = enrichedTokens.firstOrNull { it == token } - enrichedToken ?: token + enrichedTokenMap[token] ?: token } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index 9b18e1b8fd..79af422911 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -1,10 +1,13 @@ package com.tangem.data.common.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.cache.etag.DefaultETagsStore +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.* import com.tangem.data.common.quote.DefaultQuotesFetcher import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -76,4 +79,10 @@ internal object DataCommonModule { fun provideQuotesFetcher(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider): QuotesFetcher { return DefaultQuotesFetcher(tangemTechApi = tangemTechApi, dispatchers = dispatchers) } + + @Provides + @Singleton + fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore { + return DefaultETagsStore(appPreferencesStore = appPreferencesStore) + } } \ No newline at end of file From a163569c864333300a4926547b6fc36303e834ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Sep 2025 15:40:08 +0300 Subject: [PATCH 68/69] Updated on 2026-08-14 --- .../com/tangem/scenarios/BaseScenarios.kt | 14 +- .../screens/BuyTokenDetailsPageObject.kt | 2 +- .../FailedCardVerificationDialogPageObject.kt | 28 ++ .../tangem/screens/MainScreenPageObject.kt | 24 ++ .../com/tangem/screens/SwapTokenPageObject.kt | 2 +- .../kotlin/com/tangem/tests/WarningTest.kt | 54 ++++ .../sdk/mocks/content/DevWalletMockContent.kt | 270 ++++++++++++++++++ .../components/notifications/Notification.kt | 9 +- .../core/ui/test/NotificationTestTags.kt | 4 +- 9 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/FailedCardVerificationDialogPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/DevWalletMockContent.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 8355c58267..9c51a15aa5 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -4,13 +4,25 @@ import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.screens.* +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.MockContent import com.tangem.tap.domain.sdk.mocks.MockProvider import io.qameta.allure.kotlin.Allure.step -fun BaseTestCase.openMainScreen(productType: ProductType? = null, alreadyActivatedDialogIsShown : Boolean = false) { +fun BaseTestCase.openMainScreen( + productType: ProductType? = null, + mockContent: MockContent? = null, + alreadyActivatedDialogIsShown : Boolean = false +) { if (productType != null) { MockProvider.setMocks(productType) } + if (mockContent != null) { + MockProvider.setMocks(mockContent) + } step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt index 4193f3ab8b..2b4e330704 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt @@ -38,7 +38,7 @@ class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProv } val errorNotificationText: KNode = child { - hasTestTag(NotificationTestTags.TEXT) + hasTestTag(NotificationTestTags.MESSAGE) hasText(getResourceString(R.string.common_unknown_error)) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/FailedCardVerificationDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/FailedCardVerificationDialogPageObject.kt new file mode 100644 index 0000000000..a5ef4d40fb --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/FailedCardVerificationDialogPageObject.kt @@ -0,0 +1,28 @@ +package com.tangem.screens + +import com.kaspersky.kaspresso.screens.KScreen +import com.tangem.core.ui.R +import io.github.kakaocup.kakao.text.KButton +import io.github.kakaocup.kakao.text.KTextView + +object FailedCardVerificationDialogPageObject : KScreen() { + + override val layoutId: Int? = null + override val viewClass: Class<*>? = null + + val title = KTextView { + withId(R.id.alertTitle) + } + + val message = KTextView { + withId(R.id.message) + } + + val cancelButton = KButton { + withText(R.string.common_cancel) + } + + val understandButton = KButton { + withText(R.string.common_understand) + } +} \ 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 59419f5f41..43ad397162 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -8,6 +8,7 @@ import com.tangem.common.extensions.hasLazyListItemPosition import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.TokenElementsTestTags import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.core.ui.test.NotificationTestTags import com.tangem.core.ui.utils.LazyListItemPositionSemantics import com.tangem.feature.wallet.impl.R import io.github.kakaocup.compose.node.element.ComposeScreen @@ -48,6 +49,29 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_buy)) } + val notificationContainer: KNode = child { + hasTestTag(NotificationTestTags.CONTAINER) + useUnmergedTree = true + } + + val devCardNotificationIcon: KNode = child { + hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title))) + hasTestTag(NotificationTestTags.ICON) + useUnmergedTree = true + } + + val devCardNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText(getResourceString(R.string.warning_developer_card_title)) + useUnmergedTree = true + } + + val devCardNotificationMessage: KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText(getResourceString(R.string.warning_developer_card_message)) + useUnmergedTree = true + } + /** * Find token list item with title and address */ diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index dd53bc1dc5..d069f7abe1 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -57,7 +57,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } val errorNotificationText: KNode = child { - hasTestTag(NotificationTestTags.TEXT) + hasTestTag(NotificationTestTags.MESSAGE) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt new file mode 100644 index 0000000000..e3c3937047 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt @@ -0,0 +1,54 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.scenarios.openMainScreen +import com.tangem.screens.onMainScreen +import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class WarningTest : BaseTestCase() { + + @AllureId("898") + @DisplayName("Warnings: check 'Dev card' warning") + @Test + fun devCardWarningTest() { + setupHooks().run { + step("Open 'Main' screen") { + openMainScreen(mockContent = DevWalletMockContent) + } + step("Assert 'Dev card' notification title is displayed") { + onMainScreen { devCardNotificationTitle.assertIsDisplayed() } + } + step("Assert 'Dev card' notification message is displayed") { + onMainScreen { devCardNotificationMessage.assertIsDisplayed() } + } + step("Assert 'Dev card' notification icon is displayed") { + onMainScreen { devCardNotificationIcon.assertIsDisplayed() } + } + } + } + + @AllureId("3991") + @DisplayName("Warnings: check 'Dev card' warning is not displayed for release card") + @Test + fun releaseCardWarningTest() { + setupHooks().run { + step("Open 'Main' screen") { + openMainScreen() + } + step("Assert 'Dev card' notification title is not displayed") { + onMainScreen { devCardNotificationTitle.assertIsNotDisplayed() } + } + step("Assert 'Dev card' notification message is not displayed") { + onMainScreen { devCardNotificationMessage.assertIsNotDisplayed() } + } + step("Assert 'Dev card' notification icon is not displayed") { + onMainScreen { devCardNotificationIcon.assertIsNotDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/DevWalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/DevWalletMockContent.kt new file mode 100644 index 0000000000..29b0ec5fee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/DevWalletMockContent.kt @@ -0,0 +1,270 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import java.util.Date + +object DevWalletMockContent : MockContent { + + private val primaryCard = PrimaryCard( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + linkingKey = byteArrayOf( + 2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121, + -57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91, + ), + existingWalletsCount = 3, isHDWalletAllowed = true, + issuer = Card.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + manufacturer = Card.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1743759687), + signature = byteArrayOf(), + ), + walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug), + firmwareVersion = FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Sdk, + ), + isKeysImportAllowed = false, + certificate = null, + ) + + override val cardDto = CardDTO( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Sdk, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 20, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = true, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = true, + isHDWalletAllowed = true, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Secp256r1, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + ), + DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + ), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 1, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + ), + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = null, + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse( + entries = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch + publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121), + chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge + publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124), + chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + ) + + override val extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110), + chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ) + + override val successResponse = SuccessResponse(cardId = "AC05000000086747") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + primaryCard = primaryCard, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ 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 a3e97923bd..93f5aefdf0 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 @@ -116,7 +116,9 @@ internal fun NotificationBaseContainer( ) { Box { Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing12) + .testTag(NotificationTestTags.CONTAINER), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), ) { content() @@ -151,7 +153,8 @@ private fun MainContent( tint = iconTint, modifier = Modifier .size(size = iconSize) - .align(alignment = Alignment.Top), + .align(alignment = Alignment.Top) + .testTag(NotificationTestTags.ICON), ) SpacerW(width = TangemTheme.dimens.spacing10) @@ -219,7 +222,7 @@ internal fun TextsBlock( text = subtitleText, color = subtitleColor, style = TangemTheme.typography.caption2, - modifier = Modifier.testTag(NotificationTestTags.TEXT), + modifier = Modifier.testTag(NotificationTestTags.MESSAGE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt index 15a63e2186..b02a22b08f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt @@ -1,6 +1,8 @@ package com.tangem.core.ui.test object NotificationTestTags { + const val CONTAINER = "NOTIFICATION_CONTAINER" const val TITLE = "NOTIFICATION_TITLE" - const val TEXT = "NOTIFICATION_TEXT" + const val MESSAGE = "NOTIFICATION_MESSAGE" + const val ICON = "NOTIFICATION_ICON" } \ No newline at end of file From a253674fe66b8d9027e297cbaa4bab75c364e7cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Sep 2025 15:40:37 +0300 Subject: [PATCH 69/69] Updated on 2026-08-14 --- .../com/tangem/scenarios/BaseScenarios.kt | 12 + .../scenarios/DeviceSetingsScenarios.kt | 21 ++ .../tangem/scenarios/ResetCardScenarios.kt | 80 ++++++ .../screens/DeviceSettingsPageObject.kt | 61 ++++ .../com/tangem/screens/ResetCardPageObject.kt | 65 +++++ .../screens/WalletSettingsPageObject.kt | 2 +- .../kotlin/com/tangem/tests/DetailsTest.kt | 6 +- .../kotlin/com/tangem/tests/ResetCardTest.kt | 85 ++++++ .../mocks/content/BackupWalletMockContent.kt | 270 ++++++++++++++++++ .../ui/cardsettings/CardSettingsScreen.kt | 11 +- .../details/ui/resetcard/ResetCardScreen.kt | 16 +- .../ui/test/DeviceSettingsScreenTestTags.kt | 8 + .../core/ui/test/ResetCardScreenTestTags.kt | 10 + 13 files changed, 639 insertions(+), 8 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/ResetCardScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ResetCardPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/BackupWalletMockContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/ResetCardScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 9c51a15aa5..272707ea7c 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -49,4 +49,16 @@ fun BaseTestCase.synchronizeAddresses(balance: String) { step("Assert wallet balance = '$balance'") { onMainScreen { walletBalance().assertTextContains(balance) } } +} + +fun BaseTestCase.openDeviceSettingsScreen() { + step("Open wallet details") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Open 'Wallet settings' screen") { + onDetailsScreen { walletNameButton.performClick() } + } + step("Click on 'Device settings' button") { + onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt new file mode 100644 index 0000000000..d58f6ba8e3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt @@ -0,0 +1,21 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.onDeviceSettingsScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) { + step("Click on 'Scan card or ring' button") { + onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } + } + step("Assert 'Reset to Factory Settings' button title is displayed") { + onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.assertIsDisplayed() } + } + step("Assert 'Reset to Factory Settings' button subtitle is displayed") { + onDeviceSettingsScreen { resetToFactorySettingsButtonSubtitle(withBackup).assertIsDisplayed() } + } + step("Click on 'Reset to Factory Settings' button") { + onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.performClick() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/ResetCardScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/ResetCardScenarios.kt new file mode 100644 index 0000000000..670cfbce63 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/ResetCardScenarios.kt @@ -0,0 +1,80 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.screens.onResetCardScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.checkResetCardScreen(withBackup: Boolean = false) { + step("Assert title is displayed") { + onResetCardScreen { title.assertIsDisplayed() } + } + step("Assert 'Attention' image is displayed") { + onResetCardScreen { attentionImage.assertIsDisplayed() } + } + step("Assert 'Attention' subtitle is displayed") { + onResetCardScreen { subtitle.assertIsDisplayed() } + } + step("Assert description is displayed") { + onResetCardScreen { description.assertIsDisplayed() } + } + step("Assert 'Lost wallet' checkbox is displayed") { + onResetCardScreen { lostWalletAccessCheckBox.assertIsDisplayed() } + } + if (withBackup) { + step("Assert 'Lost password restore' checkbox is displayed") { + onResetCardScreen { lostPasswordRestoreCheckBox.assertIsDisplayed() } + } + } else { + step("Assert 'Lost password restore' checkbox doesn't exist") { + onResetCardScreen { lostPasswordRestoreCheckBox.assertDoesNotExist() } + } + } +} + +fun BaseTestCase.checkCheckBoxLogic(withBackup: Boolean = false) { + if (withBackup) { + step("Click on 'Lost wallet' checkbox") { + onResetCardScreen { lostWalletAccessCheckBox.performClick() } + } + step("Assert 'Lost wallet' checkbox is enabled") { + onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() } + } + step("Assert 'Reset the card' button is disabled") { + onResetCardScreen { resetCardButton.assertIsNotEnabled() } + } + step("Click on 'Lost password restore' checkbox") { + onResetCardScreen { lostPasswordRestoreCheckBox.performClick() } + } + step("Assert 'Lost password restore' checkbox is enabled") { + onResetCardScreen { lostPasswordRestoreCheckBox.assertIsEnabled() } + } + step("Assert 'Reset the card' button is enabled") { + onResetCardScreen { resetCardButton.assertIsEnabled() } + } + step("Click on 'Lost password restore' checkbox") { + onResetCardScreen { lostPasswordRestoreCheckBox.performClick() } + } + step("Assert 'Reset the card' button is disabled") { + onResetCardScreen { resetCardButton.assertIsNotEnabled() } + } + step("Click on 'Lost wallet' checkbox") { + onResetCardScreen { lostWalletAccessCheckBox.performClick() } + } + step("Click on 'Lost password restore' checkbox") { + onResetCardScreen { lostPasswordRestoreCheckBox.performClick() } + } + step("Assert 'Reset the card' button is disabled") { + onResetCardScreen { resetCardButton.assertIsNotEnabled() } + } + } else { + step("Click on 'Lost wallet' checkbox") { + onResetCardScreen { lostWalletAccessCheckBox.performClick() } + } + step("Assert 'Lost wallet' checkbox is enabled") { + onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() } + } + step("Assert 'Reset the card' button is enabled") { + onResetCardScreen { resetCardButton.assertIsEnabled() } + } + } +} diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt new file mode 100644 index 0000000000..7822f4b34e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt @@ -0,0 +1,61 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.DeviceSettingsScreenTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.wallet.R +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.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(DeviceSettingsScreenTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + val imageBlock: KNode = child { + hasTestTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK) + useUnmergedTree = true + } + + val resetToFactorySettingsButtonTitle: KNode = child { + hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE) + hasText(getResourceString(R.string.card_settings_reset_card_to_factory)) + useUnmergedTree = true + } + + val scanCardOrRingButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.scan_card_settings_button)) + useUnmergedTree = true + } + + fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child { + hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE) + useUnmergedTree = true + if (withBackup) { + hasText(getResourceString(R.string.reset_card_with_backup_to_factory_message)) + } else { + hasText(getResourceString(R.string.reset_card_without_backup_to_factory_message)) + } + } +} + +internal fun BaseTestCase.onDeviceSettingsScreen(function: DeviceSettingsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ResetCardPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ResetCardPageObject.kt new file mode 100644 index 0000000000..9bbb4eccce --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ResetCardPageObject.kt @@ -0,0 +1,65 @@ +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.ResetCardScreenTestTags +import com.tangem.wallet.R +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 androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText + +class ResetCardPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(ResetCardScreenTestTags.TITLE) + hasText(getResourceString(R.string.card_settings_reset_card_to_factory)) + useUnmergedTree = true + } + + val attentionImage: KNode = child { + hasTestTag(ResetCardScreenTestTags.ATTENTION_IMAGE) + useUnmergedTree = true + } + + val subtitle: KNode = child { + hasTestTag(ResetCardScreenTestTags.SUBTITLE) + hasText(getResourceString(R.string.common_attention)) + useUnmergedTree = true + } + + val description: KNode = child { + hasTestTag(ResetCardScreenTestTags.DESCRIPTION) + useUnmergedTree = true + } + + val lostWalletAccessCheckBox: KNode = child { + hasTestTag(ResetCardScreenTestTags.CHECKBOX) + hasAnySibling( + withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and + withText(getResourceString(R.string.reset_card_to_factory_condition_1)) + ) + useUnmergedTree = true + } + + val lostPasswordRestoreCheckBox: KNode = child { + hasTestTag(ResetCardScreenTestTags.CHECKBOX) + hasAnySibling( + withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and + withText(getResourceString(R.string.reset_card_to_factory_condition_2)) + ) + useUnmergedTree = true + } + + val resetCardButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.reset_card_to_factory_button_title)) + } +} + +internal fun BaseTestCase.onResetCardScreen(function: ResetCardPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index 6a2a606ef5..e8b554846b 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -21,7 +21,7 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi val linkMoreCardsButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.details_row_title_create_backup)) } - val cardSettingsButton: KNode = walletSettingsItem.child { + val deviceSettingsButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.card_settings_title)) } val referralProgramButton: KNode = walletSettingsItem.child { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index b60639cf4a..49fe556eba 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -55,7 +55,7 @@ class DetailsTest : BaseTestCase() { linkMoreCardsButton.assertIsDisplayed() } step("Assert 'Card Settings' button is visible") { - cardSettingsButton.assertIsDisplayed() + deviceSettingsButton.assertIsDisplayed() } step("Assert 'Referral program' button is visible") { referralProgramButton.assertIsDisplayed() @@ -105,7 +105,7 @@ class DetailsTest : BaseTestCase() { linkMoreCardsButton.assertIsNotDisplayed() } step("Assert 'Card Settings' button is visible") { - cardSettingsButton.assertIsDisplayed() + deviceSettingsButton.assertIsDisplayed() } step("Assert 'Referral program' button is visible") { referralProgramButton.assertIsDisplayed() @@ -152,7 +152,7 @@ class DetailsTest : BaseTestCase() { } onWalletSettingsScreen { step("Assert 'Card Settings' button is visible") { - cardSettingsButton.assertIsDisplayed() + deviceSettingsButton.assertIsDisplayed() } step("Assert 'Referral program' button does not exist") { referralProgramButton.assertIsNotDisplayed() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt new file mode 100644 index 0000000000..d159dc0eda --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt @@ -0,0 +1,85 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.domain.models.scan.ProductType +import com.tangem.scenarios.* +import com.tangem.tap.domain.sdk.mocks.content.BackupWalletMockContent +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 ResetCardTest : BaseTestCase() { + + @AllureId("3988") + @DisplayName("Reset card: reset Wallet 1.0 card without backup") + @Test + fun resetWallet1CardWithoutBackupTest() { + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Open 'Device settings' screen") { + openDeviceSettingsScreen() + } + step("Open 'Reset card' screen") { + openResetCardScreen() + } + step("Check 'Reset card' screen") { + checkResetCardScreen() + } + step("Check checkbox logic") { + checkCheckBoxLogic() + } + } + } + + @AllureId("3987") + @DisplayName("Reset card: reset Wallet 1.0 card with backup") + @Test + fun resetWallet1CardWithBackupTest() { + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(mockContent = BackupWalletMockContent) + } + step("Open 'Device settings' screen") { + openDeviceSettingsScreen() + } + step("Open 'Reset card' screen") { + openResetCardScreen(withBackup = true) + } + step("Check 'Reset card' screen") { + checkResetCardScreen(withBackup = true) + } + step("Check checkbox logic") { + checkCheckBoxLogic(withBackup = true) + } + } + } + + @AllureId("3974") + @DisplayName("Reset card: reset Wallet 2.0 card with backup") + @Ignore("toDo [REDACTED_TASK_KEY]: On CI Already used wallet dialog doesn't displayed") + @Test + fun resetWallet2CardWithBackupTest() { + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + } + step("Open 'Device settings' screen") { + openDeviceSettingsScreen() + } + step("Open 'Reset card' screen") { + openResetCardScreen(withBackup = true) + } + step("Check 'Reset card' screen") { + checkResetCardScreen(withBackup = true) + } + step("Check checkbox logic") { + checkCheckBoxLogic(withBackup = true) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/BackupWalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/BackupWalletMockContent.kt new file mode 100644 index 0000000000..c2c8d9ba23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/BackupWalletMockContent.kt @@ -0,0 +1,270 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import java.util.Date + +object BackupWalletMockContent : MockContent { + + private val primaryCard = PrimaryCard( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + linkingKey = byteArrayOf( + 2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121, + -57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91, + ), + existingWalletsCount = 3, isHDWalletAllowed = true, + issuer = Card.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + manufacturer = Card.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1743759687), + signature = byteArrayOf(), + ), + walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug), + firmwareVersion = FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + isKeysImportAllowed = false, + certificate = null, + ) + + override val cardDto = CardDTO( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 20, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = true, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = true, + isHDWalletAllowed = true, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Secp256r1, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 0, + hasBackup = true, + derivedKeys = mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + ), + DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + ), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 1, + hasBackup = true, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + ), + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.Active(1), + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = null, + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse( + entries = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch + publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121), + chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge + publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124), + chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + ) + + override val extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110), + chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ) + + override val successResponse = SuccessResponse(cardId = "AC05000000086747") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + primaryCard = primaryCard, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 44fdad55f0..eb76dc0b7a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -13,11 +13,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview 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.DeviceSettingsScreenTestTags import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @@ -49,7 +51,8 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing40), + .padding(bottom = TangemTheme.dimens.spacing40) + .testTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK), ) { Image( modifier = Modifier @@ -116,7 +119,9 @@ private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return LazyColumn( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(DeviceSettingsScreenTestTags.LAZY_LIST), ) { items(state.cardDetails) { val paddingBottom = when (it) { @@ -164,12 +169,14 @@ private fun CardSettings(state: CardSettingsScreenState) { text = it.titleRes.resolveReference(), color = titleColor, style = TangemTheme.typography.subtitle1, + modifier = Modifier.testTag(DeviceSettingsScreenTestTags.ITEM_TITLE), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) Text( text = it.subtitle.resolveReference(), color = subtitleColor, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE), ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index dd3093deb9..e81602a4d2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -9,12 +9,14 @@ import androidx.compose.material3.IconToggleButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* 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.ResetCardScreenTestTags import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.details.ui.common.DetailsMainButton @@ -74,6 +76,7 @@ private fun Title() { text = stringResourceSafe(id = R.string.card_settings_reset_card_to_factory), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(ResetCardScreenTestTags.TITLE), ) } @@ -82,7 +85,9 @@ private fun AlertImage() { Image( painter = painterResource(id = R.drawable.img_alert_80), contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size80), + modifier = Modifier + .size(TangemTheme.dimens.size80) + .testTag(ResetCardScreenTestTags.ATTENTION_IMAGE), ) } @@ -92,6 +97,7 @@ private fun Subtitle() { text = stringResourceSafe(id = R.string.common_attention), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(ResetCardScreenTestTags.SUBTITLE), ) } @@ -101,6 +107,7 @@ private fun Description(text: TextReference) { text = text.resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, + modifier = Modifier.testTag(ResetCardScreenTestTags.DESCRIPTION), ) } @@ -135,7 +142,11 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) .padding(vertical = TangemTheme.dimens.size16), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), ) { - IconToggleButton(checked = checkedState, onCheckedChange = onCheckedChange) { + IconToggleButton( + checked = checkedState, + onCheckedChange = onCheckedChange, + modifier = Modifier.testTag(ResetCardScreenTestTags.CHECKBOX), + ) { AnimatedContent(targetState = checkedState, label = "Update checked state") { checked -> Icon( painter = painterResource( @@ -159,6 +170,7 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) text = description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, + modifier = Modifier.testTag(ResetCardScreenTestTags.CHECKBOX_TEXT), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt new file mode 100644 index 0000000000..a995632d4f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.test + +object DeviceSettingsScreenTestTags { + const val LAZY_LIST = "DEVICE_SETTINGS_SCREEN_LAZY_LIST" + const val IMAGE_BLOCK = "DEVICE_SETTINGS_SCREEN_IMAGE_BLOCK" + const val ITEM_TITLE = "DEVICE_SETTINGS_SCREEN_ITEM_TITLE" + const val ITEM_SUBTITLE = "DEVICE_SETTINGS_SCREEN_ITEM_SUBTITLE" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ResetCardScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ResetCardScreenTestTags.kt new file mode 100644 index 0000000000..0f0409aba4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ResetCardScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object ResetCardScreenTestTags { + const val TITLE = "RESET_CARD_SCREEN_TITLE" + const val ATTENTION_IMAGE = "RESET_CARD_SCREEN_ATTENTION_IMAGE" + const val SUBTITLE = "RESET_CARD_SCREEN_SUBTITLE" + const val DESCRIPTION = "RESET_CARD_SCREEN_DESCRIPTION" + const val CHECKBOX = "RESET_CARD_SCREEN_CHECKBOX" + const val CHECKBOX_TEXT = "RESET_CARD_SCREEN_CHECKBOX_TEXT" +} \ No newline at end of file