diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 07cfce37ff..a494a9b493 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 07cfce37ff84e70081aca82d9948acb13c5c07b0 +Subproject commit a494a9b49335d891dc5836d478c843432a857621 diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 489effa20d..8cfba8dd64 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -13,20 +13,20 @@ sealed class AnalyticsParam { } sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") companion object } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") companion object { @@ -35,13 +35,13 @@ sealed class AnalyticsParam { } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") + data object AccessCode : UserCode("Access Code") } sealed class SecurityMode(val value: String) { - object AccessCode : SecurityMode("Access Code") - object Passcode : SecurityMode("Passcode") - object LongTap : SecurityMode("Long Tap") + data object AccessCode : SecurityMode("Access Code") + data object Passcode : SecurityMode("Passcode") + data object LongTap : SecurityMode("Long Tap") companion object { fun from(option: SecurityOption): SecurityMode = when (option) { @@ -56,8 +56,8 @@ sealed class AnalyticsParam { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -67,21 +67,21 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType(value = "Private Key") - object NewSeed : WalletCreationType(value = "New Seed") - object SeedImport : WalletCreationType(value = "Seed Import") + data object PrivateKey : WalletCreationType(value = "Private Key") + data object NewSeed : WalletCreationType(value = "New Seed") + data object SeedImport : WalletCreationType(value = "Seed Import") } sealed class AppTheme(val value: String) { - object System : AppTheme("System") - object Dark : AppTheme("Dark") - object Light : AppTheme("Light") + data object System : AppTheme("System") + data object Dark : AppTheme("Dark") + data object Light : AppTheme("Light") companion object { fun fromAppThemeMode(mode: AppThemeMode): AppTheme { @@ -104,6 +104,7 @@ sealed class AnalyticsParam { const val PERMISSION_TYPE = "Permission Type" const val PRODUCT_TYPE = "Product Type" const val FIRMWARE = "Firmware" + const val USER_WALLET_ID = "User Wallet ID" const val CURRENCY = "Currency" const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index c881cd0127..9ba9bbbf87 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.IntroductionProcess @@ -18,6 +19,8 @@ class CardContextInterceptor( private val scanResponse: ScanResponse, ) : ParamsInterceptor { + private val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + override fun id(): String = CardContextInterceptor.id() override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { @@ -32,6 +35,9 @@ class CardContextInterceptor( params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse) params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue + if (userWalletId != null) { + params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue + } ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { params[AnalyticsParam.CURRENCY] = it.value diff --git a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt index af2732b2d9..e4b2deeb8d 100644 --- a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt @@ -5,13 +5,15 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object ThemeModule { @Provides + @Singleton fun provideAppThemeModeHolder(): AppThemeModeHolder { return MutableAppThemeModeHolder } diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt new file mode 100644 index 0000000000..97b0f48bee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di + +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder +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 UiDependenciesModule { + + @Provides + @Singleton + fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies { + return object : UiDependencies { + override val hapticManager = hapticManager + override val appThemeModeHolder = appThemeModeHolder + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 4434311a95..f49789df71 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -8,6 +8,8 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase +import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -69,4 +71,10 @@ internal object CardDomainModule { ): GetExtendedPublicKeyForCurrencyUseCase { return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) } + + @Provides + @ViewModelScoped + fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase { + return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager) + } } \ No newline at end of file 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 da46031149..22502ca5c5 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 @@ -7,8 +7,8 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module @@ -108,17 +108,25 @@ internal object SettingsDomainModule { @Provides @ViewModelScoped fun provideShouldShowSwapPromoWalletUseCase( - swapPromoRepository: SwapPromoRepository, + promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoWalletUseCase { - return ShouldShowSwapPromoWalletUseCase(swapPromoRepository) + return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository) + } + + @Provides + @ViewModelScoped + fun provideShouldShowTravalaPromoWalletUseCase( + promoSettingsRepository: PromoSettingsRepository, + ): ShouldShowTravalaPromoWalletUseCase { + return ShouldShowTravalaPromoWalletUseCase(promoSettingsRepository) } @Provides @ViewModelScoped fun provideShouldShowSwapPromoTokenUseCase( - swapPromoRepository: SwapPromoRepository, + promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoTokenUseCase { - return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) + return ShouldShowSwapPromoTokenUseCase(promoSettingsRepository) } @Provides 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 6821d4a992..91c7d4aadb 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 @@ -346,4 +346,12 @@ internal object TokensDomainModule { ): RunPolkadotAccountHealthCheckUseCase { return RunPolkadotAccountHealthCheckUseCase(repository) } + + @Provides + @ViewModelScoped + fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase { + return GetNetworkAddressesUseCase( + networksRepository = networksRepository, + ) + } } \ No newline at end of file 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 bf4f00059e..0066c2ae37 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 @@ -4,10 +4,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -54,4 +51,10 @@ internal object TransactionDomainModule { fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { return IsFeeApproximateUseCase(feeRepository) } + + @Provides + @ViewModelScoped + fun provideValidateTransactionUseCase(transactionRepository: TransactionRepository): ValidateTransactionUseCase { + return ValidateTransactionUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt new file mode 100644 index 0000000000..8834604935 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.domain.card + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.tap.domain.sdk.TangemSdkManager + +internal class DefaultDeleteSavedAccessCodesUseCase( + private val tangemSdkManager: TangemSdkManager, +) : DeleteSavedAccessCodesUseCase { + + override suspend fun invoke(cardId: String): Either { + tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) + .doOnFailure { return it.left() } + .doOnSuccess { return Unit.right() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index efc9a7ca22..da295c57c5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -5,11 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel import dagger.hilt.android.AndroidEntryPoint @@ -24,10 +23,7 @@ import javax.inject.Inject internal class AddCustomTokenFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index 5db91d6354..6bb0c880af 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -23,12 +23,12 @@ object ContractAddressValidator { private fun validateAddress(blockchain: Blockchain, address: String): Boolean { return when (blockchain) { - Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> { - SuccessAddressValidator.validate(address) - } - else -> { - blockchain.validateAddress(address) - } + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.BinanceTestnet, + Blockchain.Cardano, + -> SuccessAddressValidator.validate(address) + else -> blockchain.validateAddress(address) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt index 0d0a7622ed..d48050ec85 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -5,11 +5,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -17,10 +16,7 @@ import javax.inject.Inject internal class AppCurrencySelectorFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel: AppCurrencySelectorViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 22f7523945..2efc912cca 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -7,9 +7,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store @@ -20,10 +19,7 @@ import javax.inject.Inject internal class AppSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var appCurrencyRepository: AppCurrencyRepository diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index d9e740b39b..c81e2d819e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -5,9 +5,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -17,10 +16,7 @@ import javax.inject.Inject internal class CardSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel: CardSettingsViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 70af8f9ca9..7a365126b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -5,9 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -23,10 +22,7 @@ class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var walletsRepository: WalletsRepository diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 39c6afd544..1522c07ed3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -5,9 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -18,10 +17,7 @@ import javax.inject.Inject internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel = ResetCardViewModel(store) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index e315a727d3..c44b9ca3fa 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -5,9 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -18,10 +17,7 @@ import javax.inject.Inject internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel = SecurityModeViewModel(store) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 4e69260a90..19dd0771bc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -8,9 +8,8 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store @@ -22,10 +21,7 @@ import javax.inject.Inject internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel: WalletConnectViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index a108298d21..8e081474fb 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -12,10 +12,9 @@ import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.redux.AppState @@ -31,10 +30,7 @@ import javax.inject.Inject class HomeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private var homeState: MutableState = mutableStateOf(store.state.homeState) 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 6c4c24f485..6840975e44 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 @@ -14,6 +14,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -39,6 +40,7 @@ internal class MainViewModel @Inject constructor( private val blockchainSDKFactory: BlockchainSDKFactory, private val userWalletsListManager: UserWalletsListManager, private val walletManagersFacade: WalletManagersFacade, + private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -61,6 +63,7 @@ internal class MainViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } updateAppCurrencies() + updateSendFeatureToggle() observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() @@ -108,6 +111,12 @@ internal class MainViewModel @Inject constructor( } } + private fun updateSendFeatureToggle() { + viewModelScope.launch(dispatchers.main) { + sendFeatureToggles.fetchNewSendEnabled() + } + } + private fun observeFlips() { listenToFlipsUseCase().launchIn(viewModelScope) } diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt index c6c6c31809..ef72720cad 100644 --- a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt @@ -9,10 +9,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.ModalNotification import com.tangem.tap.features.main.ui.components.ModalNotificationContent @@ -23,10 +22,7 @@ import javax.inject.Inject internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel: MainViewModel by activityViewModels() diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index 630f9ca789..d0d7c64fb5 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -11,10 +11,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent @@ -26,10 +25,7 @@ import javax.inject.Inject internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies override val expandedHeightFraction: Float = .98f diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 519637bd2f..86a40c5ccf 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -4,7 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras @@ -20,6 +20,7 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token @@ -31,7 +32,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.domain.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee @@ -170,11 +170,7 @@ private fun sendTransaction( transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) } transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) } transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } - transactionExtras.hederaMemoState?.memo?.let { - txData = txData.copy( - extras = HederaTransactionBuilder.HederaTransactionExtras(it), - ) - } + transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) } transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) } scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index 27a2ae288c..272c773c95 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -5,11 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel import dagger.hilt.android.AndroidEntryPoint @@ -24,10 +23,7 @@ import javax.inject.Inject internal class TokensListFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 7e1497ce01..60ab61900f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -15,11 +15,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -32,10 +31,7 @@ import javax.inject.Inject internal class WelcomeFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies override fun onStart() { super.onStart() diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 934f519967..a04a551fc2 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -8,7 +8,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras @@ -141,7 +141,7 @@ class TransactionManagerImpl( Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } Blockchain.Cosmos -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } @@ -224,26 +224,33 @@ class TransactionManagerImpl( when (fee.data) { is TransactionFee.Single -> { val normalFee = (fee.data as TransactionFee.Single).normal - val singleFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - ) - ProxyFees.SingleFee( - singleFee = singleFee, - ) + val singleFee = if (normalFee as? Fee.CardanoToken != null) { + ProxyFee.CardanoToken( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + minAdaValue = normalFee.minAdaValue, + ) + } else { + ProxyFee.Common( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + ) + } + + ProxyFees.SingleFee(singleFee = singleFee) } is TransactionFee.Choosable -> { val choosableFee = fee.data as TransactionFee.Choosable ProxyFees.MultipleFees( - minFee = ProxyFee( + minFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ), - normalFee = ProxyFee( + normalFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ), - priorityFee = ProxyFee( + priorityFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ), @@ -300,15 +307,15 @@ class TransactionManagerImpl( is Result.Success -> { val choosableFee = fee.data - val minProxyFee = ProxyFee( + val minProxyFee = ProxyFee.Common( gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) - val normalProxyFee = ProxyFee( + val normalProxyFee = ProxyFee.Common( gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) - val priorityProxyFee = ProxyFee( + val priorityProxyFee = ProxyFee.Common( gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) @@ -463,7 +470,7 @@ class TransactionManagerImpl( scale = blockchain.decimals(), mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), ) - val minFee = ProxyFee( + val minFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -471,7 +478,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val normalFee = ProxyFee( + val normalFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -479,7 +486,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val priorityFee = ProxyFee( + val priorityFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index a20223d2cb..b77078a426 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -3,46 +3,46 @@ package com.tangem.core.analytics.models sealed class AnalyticsParam { sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") - object CustomToken : CardBalanceState("Custom Token") - object BlockchainError : CardBalanceState("Blockchain Error") - object NoRate : CardBalanceState("No Rate") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") + data object CustomToken : CardBalanceState("Custom Token") + data object BlockchainError : CardBalanceState("Blockchain Error") + data object NoRate : CardBalanceState("No Rate") companion object } sealed class TokenBalanceState(val value: String) { - object Empty : TokenBalanceState("Empty") - object Full : TokenBalanceState("Full") + data object Empty : TokenBalanceState("Empty") + data object Full : TokenBalanceState("Full") } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Disliked : RateApp("Disliked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Disliked : RateApp("Disliked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") } sealed class OrganizeSortType(val value: String) { - object ByBalance : OrganizeSortType("By Balance") - object Manually : OrganizeSortType("Manually") + data object ByBalance : OrganizeSortType("By Balance") + data object Manually : OrganizeSortType("Manually") } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") - object Passcode : UserCode("Passcode") + data object AccessCode : UserCode("Access Code") + data object Passcode : UserCode("Passcode") } sealed class AccessCodeRecoveryStatus(val value: String) { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -52,9 +52,9 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class ScreensSources(val value: String) { @@ -86,8 +86,8 @@ sealed class AnalyticsParam { val permissionType: String, ) : TxSentFrom("Approve"), TxData - object WalletConnect : TxSentFrom("WalletConnect") - object Sell : TxSentFrom("Sell") + data object WalletConnect : TxSentFrom("WalletConnect") + data object Sell : TxSentFrom("Sell") } sealed interface TxData { @@ -97,10 +97,11 @@ sealed class AnalyticsParam { } sealed class FeeType(val value: String) { - object Fixed : FeeType("Fixed") - object Min : FeeType("Min") - object Normal : FeeType("Normal") - object Max : FeeType("Max") + data object Fixed : FeeType("Fixed") + data object Min : FeeType("Min") + data object Normal : FeeType("Normal") + data object Max : FeeType("Max") + data object Custom : FeeType("Custom") companion object { fun fromString(feeType: String): FeeType { @@ -116,13 +117,13 @@ sealed class AnalyticsParam { } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType("Private key") - object NewSeed : WalletCreationType("New seed") - object SeedImport : WalletCreationType("Seed import") + data object PrivateKey : WalletCreationType("Private key") + data object NewSeed : WalletCreationType("New seed") + data object SeedImport : WalletCreationType("Seed import") } sealed class WalletType(val value: String) { - object MultiCurrency : WalletType(value = "Multicurrency") + data object MultiCurrency : WalletType(value = "Multicurrency") class SingleCurrency(currencyName: String) : WalletType(currencyName) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt index fa9140eea1..668a77c321 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt @@ -13,6 +13,7 @@ data class PromotionInfoResponse( data class BannerState( @Json(name = "timeline") val timeline: Timeline, @Json(name = "status") val status: String, + @Json(name = "link") val link: String?, ) @JsonClass(generateAdapter = true) 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 0a5dcf3fd9..eb9e2a434c 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 @@ -10,6 +10,7 @@ import retrofit2.http.* * [REDACTED_AUTHOR] */ +@Suppress("TooManyFunctions") interface TangemTechApi { @GET("coins") @@ -126,4 +127,7 @@ interface TangemTechApi { @Header("card_id") cardId: String, @Path("account_id") accountId: Int, ): ApiResponse + + @GET("features") + suspend fun getFeatures(): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt new file mode 100644 index 0000000000..2d72021cd0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class FeaturesResponse( + @Json(name = "send") val isNewSendEnabled: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index f7565b73c2..fe5ad1ad8e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -99,6 +99,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey, chiaTangemApiKey = configValues.chiaTangemApiKey, + polygonScanApiKey = configValues.polygonScanApiKey, ), amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 5d2560be9e..91ae26a0d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -41,6 +41,7 @@ class ConfigValueModel( val chiaTangemApiKey: String?, val devExpress: ExpressModel?, val express: ExpressModel?, + val polygonScanApiKey: String?, ) @JsonClass(generateAdapter = true) 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 4dfd9d6966..cfafab3c05 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 @@ -61,6 +61,10 @@ object PreferencesKeys { booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown") } + val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy { + booleanPreferencesKey(name = "isWalletTravalaPromoShown") + } + val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 7de6a6f134..d91a3988e8 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -9,11 +9,11 @@ }, { "name": "REDESIGNED_SEND_SCREEN_ENABLED", - "version": "5.9.1" + "version": "5.10.0" }, { "name": "LOCAL_USER_LOGS_ENABLED", - "version": "5.10.0" + "version": "5.11.0" }, { "name": "GENERATE_XPUB_ENABLED", @@ -26,5 +26,9 @@ { "name": "TOKEN_LIST_LCE_ENABLED", "version": "5.10.0" + }, + { + "name": "CARDANO_TOKENS_SUPPORT_ENABLED", + "version": "5.11.0" } ] diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b8a5d12fe9..53f6a46e4e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,16 +4,12 @@ Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. Обратиться в поддержку - Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. - Ваши предложения отправлены - Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -28,8 +24,6 @@ Тёмная Светлая Как в системе - При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства - Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -57,10 +51,12 @@ Заводские настройки Тип безопасности Настройки карты - Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s - Чтобы совершить транзакцию %1$s, вы должны внести некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA. (рекомендуется баланс в размере 5 ADA) - Недостаточно ADA для отправки токена - Сеть Cardano не позволяет вам оставлять на своем балансе сумму ниже минимального значения %1$s, если у вас есть токены. Пожалуйста, убедитесь, что у вас осталось немного ADA. + Помимо сетевой комиссий, сеть Cardano взимает %1$s ADA при транзакции с токеном %2$s + Требования к транзакции Cardano + Чтобы совершить транзакцию %1$s, внесите некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA (рекомендуется 5 ADA) + Недостаточно ADA для транзакции + Вы должны поддерживать некоторое количество ADA, поскольку у вас на балансе есть токены в сети Cardano + Недостаточно ADA Принять Доступ запрещен Применить @@ -81,7 +77,6 @@ Создать Удалить Отключено - Отключить Готово Включить Включено @@ -102,6 +97,7 @@ Заблокирован Основная сеть Сетевая комиссия + Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии Далее Нет Нет адреса @@ -114,7 +110,6 @@ Отклонить Перезагрузить Переименовать - Повторить Сохранить изменения Искать Поиск токенов @@ -138,7 +133,6 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно - Предупреждение Да Адрес контракта скопирован! Доступные сети @@ -185,7 +179,6 @@ Скрывать балансы жестом переворота Эмитент Подписано - Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -267,6 +260,9 @@ Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии Токены + Забронировать + Оплатите его криптой и сэкономьте **50 долларов** через нашего партнера Travala: **%1s - %2s** + Забронируйте отпуск с Tangem Добавить Изменить Рыночная капитализация @@ -445,11 +441,13 @@ Причина: %1$s\nКод: %2$s Транзакция не выполнена Сумма + Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte. %1$s, %2$s Адрес Код назначения Введите адрес Адрес совпадает с адресом кошелька + Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. Tag @@ -468,26 +466,23 @@ Всё Максимальная сумма Комиссия не превысит - Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Допустим ввод только цифр - Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Оставить %s Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия - Ввиду особенности сети Tezos комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. + Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s. Комиссия повышена Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. Сумма отправки не может быть менее %s + Оставить %s Уменьшить на %s - Уменьшить до %s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. @@ -497,7 +492,7 @@ Последние Получатель Неверный адрес - Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов. Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки @@ -506,7 +501,7 @@ Отправка Нажмите на любое поле, чтобы изменить его Отправка %s - Вы отправляете %1$s, включая комиссию сети %2$s + Вы отправляете **%1$s**, включая комиссию сети %2$s Отправка %s Всего %1$s и %2$s будет отправлено @@ -535,7 +530,6 @@ Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии Подтвердить Текущая транзакция Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. @@ -560,7 +554,6 @@ Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. - Выберите адрес Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -568,7 +561,7 @@ Скрыть токен %1$s токен в сети %%image%% %2$s Токен в сети %%image%% %1$s - Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. + Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети Невозможно скрыть %s Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии @@ -597,6 +590,7 @@ Вы уверены, что хотите удалить этот кошелек? Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой + Кошелек с именем %s уже существует Имя кошелька Переименование кошелька Разблокировать все @@ -643,6 +637,8 @@ Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. Ошибка активации + По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain. + Отключение сети BNB Beacon Chain Можно лучше Нравится Понятно! @@ -698,7 +694,6 @@ Необходима плата за аренду сети %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. Недостаточно %1$s для оплаты комиссии сети - Транзакция в обработке Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ef843270c8..4f7f952256 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,16 +4,12 @@ Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Request support - Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. - Thank you for your feedback. We will respond as soon as possible - Your suggestions were sent - Please try to tap the card exactly as shown in the animation or request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -29,8 +25,6 @@ Dark Light System default - If system is selected, the app will auto-adjust based on your device\'s system settings - System Theme App Settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -56,10 +50,12 @@ Reset to Factory Settings Security Mode Card Settings - Due to the peculiarities of the Cardano network, when transacting the %1$s token, in addition to the network commission, %2$s will be charged - To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value. (5 ADA balance recommended) - Insufficient ADA to token transfer - The Cardano network does not allow you to leave an amount on your balance below the minimum value %1$s if you have tokens. Please ensure that you have some ADA remaining. + In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token + Cardano transaction requirements + To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended) + Insufficient ADA for token transfer + You must maintain some ADA because you have some tokens on the Cardano blockchain + Not enough ADA Accept Access denied Apply @@ -80,7 +76,6 @@ Create Delete Disabled - Disconnect Done Enable Enabled @@ -101,6 +96,7 @@ Locked Main network Network fee + Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level Next No No address @@ -113,7 +109,6 @@ Reject Reload Rename - Retry Save changes Search Search tokens @@ -137,7 +132,6 @@ I understand There was an error. Please try again. Unreachable - Warning Yes Contract address copied! Available networks @@ -184,7 +178,6 @@ Flip-to-Hide Balances Issuer Signed - If you forget the code you will lose access to your funds. Code recovery is not possible. Details Check your internet connection or switch to a different network Terms of Service @@ -267,6 +260,9 @@ Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s Swap with Changelly, %s fees Tokens + Book now + Save **$50** while booking via our partner Travala: **%1s - %2s** + Book your holidays with Tangem and pay in crypto Add Edit Coin market cap @@ -442,11 +438,13 @@ Amount Base fee Represents the part of the transaction fee that goes to the miner + You can set your transaction fee by adjusting the value in the Satoshi per vByte field. %1$s, %2$s Address Destination Tag Enter address Address is the same as wallet address + The fee that will be charged for your transaction. You can set your own value. Invalid Tag. It won\'t be added to the transaction. Invalid Memo. It won\'t be added to the transaction. Tag @@ -465,26 +463,23 @@ Max Maximum amount Max fee - The fee that will be charged for your transaction. You can set your own value. Numbers only for Destination Tag - Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - Leave %s The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance. Existential deposit The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. Custom fee is high - Due to the peculiarities of the Tezos network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %s. + Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s. The fee is higher The included commission exceeds the transfer amount, leading to a negative value Invalid amount The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. Target account is not created. Please change the amount to send. The amount to send must be at least %s + Leave %s Reduce by %s - Reduce to %s Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. @@ -496,7 +491,7 @@ Recent Recipient Not a valid address - Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + Ensure the receiving wallet address is on the %s network to avoid losing your tokens 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 My wallets @@ -505,7 +500,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 Sending %s Total %1$s and %2$s will be sent @@ -535,7 +530,6 @@ Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Sending amount will be reduced to cover the selected fee level Approve Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. @@ -560,7 +554,6 @@ Selling funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. - Choose address Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -568,7 +561,7 @@ Hide token %1$s token in %%image%% %2$s network Token in %%image%% %1$s network - The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. + The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees @@ -597,6 +590,7 @@ Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one + The wallet with name %s already exists Wallet name Rename Wallet Unlock all @@ -643,6 +637,8 @@ Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. Activation error + According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service to transfer them to the BNB Smart Chain network. + BNB Beacon Chain will shut down Could be better Like it Ok, Got it! @@ -674,7 +670,7 @@ This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s This token must be associated with your Hedera account before you can receive it Associate your token - Hot enough %s. Top up your Hedera account to associate this token + Not enough %s. Top up your Hedera account to associate this token Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. @@ -696,7 +692,6 @@ Network rent fee required %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. Insufficient %1$s to cover network fee - Transaction pending The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 961dc949c5..e7c5bfc4e7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -46,4 +46,5 @@ dependencies { implementation(deps.zxing.qrCore) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.markdown) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt new file mode 100644 index 0000000000..62e19d4af6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui + +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder + +interface UiDependencies { + + val hapticManager: HapticManager + + val appThemeModeHolder: AppThemeModeHolder +} \ No newline at end of file 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 73a58d56a1..d39c09ecce 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 @@ -95,10 +95,14 @@ fun AmountTextField( SimpleTextField( value = value, onValueChange = { newText -> - if (decimalFormat.isValidSymbols(newText)) { - val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) - onValueChange(trimmed) - } + onValueChange( + prepareEnter( + oldValue = value, + newValue = newText, + decimalFormat = decimalFormat, + decimals = decimals, + ), + ) }, textStyle = textStyle.copy( fontSize = fontSize, @@ -117,8 +121,37 @@ fun AmountTextField( } } +private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String { + val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator + return if (decimalFormat.isValidSymbols(newValue)) { + val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString() + ?: if (newValue.isBlank()) "" else oldValue + val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) { + when { + parsedValue.findLast { it == COMMA_SEPARATOR } != null -> { + parsedValue.replace(COMMA_SEPARATOR, decimalSymbol) + } + parsedValue.findLast { it == POINT_SEPARATOR } != null -> { + parsedValue.replace(POINT_SEPARATOR, decimalSymbol) + } + else -> parsedValue + } + } else { + parsedValue + } + val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) { + replacedWithSymbol.plus(decimalSymbol) + } else { + replacedWithSymbol + } + decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals) + } else { + oldValue + } +} + private fun DecimalFormat.isValidSymbols(text: String): Boolean { - return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) + return checkDecimalSeparatorDuplicate(text) } // region preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index ff2bf968ae..59128a93a9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -38,6 +38,7 @@ fun SimpleTextField( keyboardActions: KeyboardActions = KeyboardActions.Default, color: Color = TangemTheme.colors.text.primary1, textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), + placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, @@ -108,6 +109,7 @@ fun SimpleTextField( value = value, textStyle = textStyle, textValue = textValue, + color = placeholderColor, ) }, modifier = modifier @@ -122,6 +124,7 @@ private fun SimpleTextPlaceholder( value: String, textStyle: TextStyle, textValue: @Composable () -> Unit, + color: Color = TangemTheme.colors.text.disabled, ) { Box { if (value.isBlank() && placeholder != null) { @@ -132,7 +135,7 @@ private fun SimpleTextPlaceholder( Text( text = it.resolveReference(), style = textStyle, - color = TangemTheme.colors.text.disabled, + color = color, ) } } 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 6d339e48dd..c572bb3f83 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 @@ -51,12 +51,14 @@ fun Notification( modifier: Modifier = Modifier, containerColor: Color? = null, iconTint: Color? = null, + isEnabled: Boolean = true, ) { BaseContainer( buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier, containerColor = containerColor, + isEnabled = isEnabled, ) { Column( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), @@ -67,15 +69,16 @@ fun Notification( iconTint = iconTint, title = config.title, subtitle = config.subtitle, - isClickableComponent = config.onClick != null, + isClickableComponent = isEnabled && config.onClick != null, ) - Buttons(state = config.buttonsState) + Buttons(state = config.buttonsState, isEnabled = isEnabled) } CloseableIconButton( onClick = config.onCloseClick, modifier = Modifier.align(alignment = Alignment.TopEnd), + isEnabled = isEnabled, ) } } @@ -85,6 +88,7 @@ private fun BaseContainer( buttonsState: NotificationConfig.ButtonsState?, onClick: (() -> Unit)?, modifier: Modifier = Modifier, + isEnabled: Boolean = true, containerColor: Color? = null, content: @Composable BoxScope.() -> Unit, ) { @@ -101,7 +105,7 @@ private fun BaseContainer( modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size62) .fillMaxWidth(), - enabled = onClick != null, + enabled = onClick != null && isEnabled, shape = TangemTheme.shapes.roundedCornersXMedium, color = containerColor ?: tempContainerColor, ) { @@ -179,27 +183,31 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) { } @Composable -private fun Buttons(state: NotificationButtonsState?) { +private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true) { when (state) { - is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state) - is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state) - is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state) + is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton( + config = state, + isEnabled = isEnabled, + ) + is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled) + is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled) null -> Unit } } @Composable -private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) { +private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig, isEnabled: Boolean = true) { SecondaryButton( text = config.text.resolveReference(), onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } @Composable -private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) { +private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig, isEnabled: Boolean = true) { if (config.iconResId != null) { PrimaryButtonIconEnd( text = config.text.resolveReference(), @@ -207,6 +215,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } else { PrimaryButton( @@ -214,18 +223,20 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } } @Composable -private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { +private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEnabled: Boolean = true) { Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) { SecondaryButton( text = config.secondaryText.resolveReference(), onClick = config.onSecondaryClick, modifier = Modifier.weight(weight = 1f), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) PrimaryButton( @@ -233,12 +244,13 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { onClick = config.onPrimaryClick, modifier = Modifier.weight(weight = 1f), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } } @Composable -private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { +private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true) { AnimatedVisibility(visible = onClick != null, modifier = modifier) { onClick ?: return@AnimatedVisibility @@ -255,6 +267,7 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod interactionSource = remember { MutableInteractionSource() }, indication = LocalIndication.current, role = Role.Button, + enabled = isEnabled, onClick = onClick, ), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 00945dbb44..076c60da41 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -49,7 +49,11 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size62) .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium), + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable( + enabled = config.onClick != null, + onClick = config.onClick ?: {}, + ), ) { val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt new file mode 100644 index 0000000000..9e6ba3db1d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt @@ -0,0 +1,215 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.ScaleFactor +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemColorPalette.White +import com.tangem.core.ui.res.TangemTheme + +/** + * Travala notification with image background + * @see Travala Promo + */ +@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") +@Composable +fun TravalaNotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + + Box( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(Color.Black) + .clickable( + enabled = config.onClick != null, + onClick = config.onClick ?: {}, + ), + propagateMinConstraints = true, + contentAlignment = Alignment.TopStart, + ) { + val density = LocalDensity.current + Image( + painter = painterResource(R.drawable.img_travala_banner_promo_background), + contentDescription = null, + contentScale = TravalaBackgroundScale(density), + alignment = Alignment.TopStart, + modifier = Modifier + .matchParentSize() + .wrapContentSize(unbounded = true, align = Alignment.TopStart) + .align(Alignment.TopStart), + ) + Image( + painter = painterResource(R.drawable.img_travala_banner_promo_background_2), + contentDescription = null, + contentScale = TravalaBackgroundScale(density), + alignment = Alignment.TopStart, + modifier = Modifier + .matchParentSize() + .wrapContentSize(unbounded = true, align = Alignment.TopEnd) + .align(Alignment.TopEnd), + ) + Column { + Row { + Box(modifier = Modifier.size(87.dp)) + Column( + Modifier + .weight(1f) + .padding(top = TangemTheme.dimens.spacing12), + ) { + Text( + text = config.title.resolveReference(), + style = TangemTheme.typography.button.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.constantWhite, + ) + SpacerH8() + Text( + text = formatSubtitle(config.subtitle.resolveReference()), + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.constantWhite, + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing2, + ) + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { + config.onCloseClick?.invoke() + }, + ) + } + + TangemButton( + text = button?.text?.resolveReference().orEmpty(), + icon = TangemButtonIconPosition.None, + onClick = button?.onClick ?: {}, + colors = TangemButtonColors( + backgroundColor = White.copy(alpha = 0.3f), + contentColor = White, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ), + enabled = true, + showProgress = false, + modifier = Modifier + .padding(TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } + } +} + +private const val TRAVALA_BACKGROUND_SRC_IMG_SCALE = 4 + +private class TravalaBackgroundScale( + val density: Density, +) : ContentScale { + override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor { + with(density) { + val originalWidth = (srcSize.width / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() + val widthScale = originalWidth / srcSize.width + val originalHeight = (srcSize.height / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() + val heightScale = originalHeight / srcSize.height + return ScaleFactor(widthScale, heightScale) + } + } +} + +@Composable +private fun formatSubtitle(subtitle: String): AnnotatedString { + val pattern = Regex("\\*\\*(.*?)\\*\\*") + var startIndex = 0 + val annotatedString = buildAnnotatedString { + pattern.findAll(subtitle).forEach { matchResult -> + val index = matchResult.range.first + val matchedValue = matchResult.groups[1]?.value ?: "" + + // appends unformatted part + append(subtitle.substring(startIndex, index)) + + // applies style on ^^-wrapped parts + withStyle(SpanStyle(fontWeight = TangemTheme.typography.caption1.fontWeight)) { + append(matchedValue) + } + + // goes to next part + startIndex = matchResult.range.last + 1 + } + + // appends remaining ending if exists + append(subtitle.substring(startIndex)) + } + + return annotatedString +} + +//region preview +@Preview +@Composable +private fun TravalaNotificationWithBackgroundPreview() { + TangemTheme { + TravalaNotificationWithBackground( + config = NotificationConfig( + title = resourceReference( + id = R.string.main_travala_promotion_title, + ), + subtitle = resourceReference( + id = R.string.main_travala_promotion_description, + formatArgs = wrappedList("May 13", "June 12"), + ), + iconResId = R.drawable.img_swap_promo, + backgroundResId = R.drawable.img_travala_banner_promo_background, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.token_swap_promotion_button), + onClick = {}, + ), + ), + ) + } +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt new file mode 100644 index 0000000000..35cc8f6903 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt @@ -0,0 +1,114 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Snackbar to inform the user about copying text to the clipboard + * + * @param message message + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(message: TextReference, modifier: Modifier = Modifier) { + BaseSnackbar(message = message, modifier = modifier) +} + +/** + * Snackbar to inform the user about copying text to the clipboard. + * + * @param snackbarData this is needed to better support Material3.SnackbarHost, but only supports the message field + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(snackbarData: SnackbarData, modifier: Modifier = Modifier) { + BaseSnackbar(message = stringReference(snackbarData.visuals.message), modifier = modifier) +} + +@Composable +private fun BaseSnackbar(message: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(color = TangemTheme.colors.icon.secondary, shape = TangemTheme.shapes.roundedCorners8) + .heightIn(min = TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing14, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + MarkIcon() + + MessageText(text = message, modifier = Modifier.weight(weight = 1f, fill = false)) + } +} + +@Composable +private fun MarkIcon() { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size20), + tint = TangemTheme.colors.icon.accent, + ) +} + +@Composable +private fun MessageText(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + modifier = modifier, + color = TangemTheme.colors.text.disabled, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_CopiedTextSnackbar( + @PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference, +) { + TangemTheme(isDark = false) { + CopiedTextSnackbar(message = message) + } +} + +private class CopiedTextSnackbarDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + stringReference(value = "Copied!"), + stringReference(value = "Contract address copied!"), + stringReference(value = "Coooooooooooontract addreeeeeeeeeeeeeeeess coooooooooooooooopied!"), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt new file mode 100644 index 0000000000..3cff3dd5c0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt @@ -0,0 +1,25 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * SnackbarHost to inform the user about copying text to the clipboard + * Based on Material3 component. It's best way to show [CopiedTextSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { + SnackbarHost(hostState = hostState, modifier = modifier) { + CopiedTextSnackbar(snackbarData = it) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt new file mode 100644 index 0000000000..70cacc3a82 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt @@ -0,0 +1,84 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar. + * Based on Material3 component. It can be presented as one line or multi lines snackbar – depends on text length. + * + * @param data snackbar data + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + Snackbar( + modifier = modifier, + action = { + ActionButton(label = data.visuals.actionLabel, onClick = data::performAction) + }, + actionOnNewLine = actionOnNewLine, + shape = TangemTheme.shapes.roundedCorners8, + containerColor = TangemTheme.colors.icon.secondary, + ) { + MessageText(text = data.visuals.message) + } +} + +@Composable +private fun ActionButton(label: String?, onClick: () -> Unit) { + if (!label.isNullOrBlank()) { + TextButton( + onClick = onClick, + colors = ButtonDefaults.textButtonColors( + contentColor = TangemTheme.colors.text.primary2, + ), + content = { + Text( + text = label, + maxLines = 1, + style = TangemTheme.typography.button, + ) + }, + ) + } +} + +@Composable +private fun MessageText(text: String) { + Text( + text = text, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) +} + +/** + * IMPORTANT! + * Preview doesn't work correctly, check on device or start [TangemSnackbarHost]'s preview in interactive mode * + */ +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + TangemSnackbar(data = model.snackbarData, actionOnNewLine = model.actionOnNewLine) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt new file mode 100644 index 0000000000..d1cd229f6d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt @@ -0,0 +1,48 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar host. + * Based on Material3 component. It's best way to show [TangemSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + SnackbarHost(hostState = hostState, modifier = modifier) { data -> + TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine) + } +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + val snackbarHostState = remember(::SnackbarHostState) + + TangemSnackbarHost(hostState = snackbarHostState, actionOnNewLine = model.actionOnNewLine) + + LaunchedEffect(key1 = null) { + snackbarHostState.showSnackbar(visuals = model.snackbarData.visuals) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt new file mode 100644 index 0000000000..e2bdbf3ecf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider + +internal data class TangemSnackbarModel(val snackbarData: SnackbarData, val actionOnNewLine: Boolean) + +internal class TangemSnackbarModelProvider : CollectionPreviewParameterProvider( + listOf( + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = true, + ), + ), +) { + + companion object { + + fun createTangemSnackbarModel( + message: String, + actionLabel: String, + actionOnNewLine: Boolean, + ): TangemSnackbarModel { + return TangemSnackbarModel( + snackbarData = createSnackbarData(message, actionLabel), + actionOnNewLine = actionOnNewLine, + ) + } + + private fun createSnackbarData(message: String, actionLabel: String): SnackbarData { + return object : SnackbarData { + + override val visuals: SnackbarVisuals + get() = object : SnackbarVisuals { + override val message: String = message + override val actionLabel: String = actionLabel + override val duration: SnackbarDuration = SnackbarDuration.Short // Never-mind + override val withDismissAction: Boolean = false // Never-mind + } + + override fun dismiss() = Unit + override fun performAction() = Unit + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt new file mode 100644 index 0000000000..408e9130ee --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -0,0 +1,56 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +/** Markdown parser */ +@Composable +fun rememberMarkdownParser() = remember { + MarkdownParser(CommonMarkFlavourDescriptor()) +} + +/** + * Styling markdown tree recursively + * + * @param markdownText original text + * @param node current processed node + */ +@Composable +fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder { + when (node.type) { + MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> { + node.children.forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + MarkdownElementTypes.STRONG -> { + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + node.children + .drop(2) + .dropLast(2) + .forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + } + else -> { + append(node.getTextInNode(markdownText).toString()) + } + } + return this +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 766815d328..2f22994f64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import org.intellij.markdown.MarkdownElementTypes /** * Utility class for creating text as [String] or [StringRes]. @@ -160,6 +163,29 @@ fun TextReference.resolveReference(resources: Resources): String { } } +/** Resolve [TextReference] as [AnnotatedString] */ +@Composable +fun TextReference.resolveAnnotatedReference(): AnnotatedString { + return when (this) { + is TextReference.Res -> { + val args = formatArgs + .map { if (it is TextReference) it.resolveReference() else it } + .toTypedArray() + + formatAnnotated(stringResource(id = id, *args)) + } + is TextReference.PluralRes -> formatAnnotated( + pluralStringResource(id, count, *formatArgs.toTypedArray()), + ) + is TextReference.Str -> formatAnnotated(value) + is TextReference.Combined -> buildAnnotatedString { + refs.forEach { + append(formatAnnotated(it.resolveReference())) + } + } + } +} + /** Concatenate [this] reference with [ref] */ operator fun TextReference.plus(ref: TextReference): TextReference { return when (this) { @@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference { is TextReference.Str, -> TextReference.Combined(refs = wrappedList(this, ref)) } +} + +@Composable +private fun formatAnnotated(rawString: String): AnnotatedString { + val markdownDescriptor = rememberMarkdownParser() + val parsedTree = markdownDescriptor.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true) + + return buildAnnotatedString { + appendMarkdown(markdownText = rawString, node = parsedTree) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index 8de596d9d4..4c2faae280 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -8,9 +8,8 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.apptheme.model.AppThemeMode /** @@ -23,14 +22,9 @@ import com.tangem.domain.apptheme.model.AppThemeMode internal interface ComposeScreen { /** - * The holder for managing the current application theme mode. + * The holder for ui dependencies. */ - val appThemeModeHolder: AppThemeModeHolder - - /** - * Haptic manager. - */ - val hapticManager: HapticManager + val uiDependencies: UiDependencies /** * The screen modifier. @@ -59,11 +53,11 @@ internal interface ComposeScreen { internal fun ComposeScreen.createComposeView(context: Context): ComposeView { return ComposeView(context).apply { setContent { - val appThemeMode by appThemeModeHolder.appThemeMode + val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), - hapticManager = hapticManager, + hapticManager = uiDependencies.hapticManager, ) { ScreenContent(modifier = screenModifier) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 0ea40469e2..d42121d3f0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.utils +import android.text.format.DateFormat import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -55,6 +56,16 @@ object DateTimeFormatters { .withLocale(Locale.getDefault()) } + /** + * In API version < 24, there may be some problems with getting the best date and time format pattern. + */ + val dateMMMMd: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d")) + .toFormatter() + .withLocale(Locale.getDefault()) + } + val dateTimeFormatter: DateTimeFormatter by lazy { DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt index 5006df1d7a..2d685763e3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols import java.util.Locale private const val TEXT_CHUNK_THOUSAND = 3 -private const val POINT_SEPARATOR = '.' -private const val COMMA_SEPARATOR = ',' private const val SCIENTIFIC_NOTATION = 'e' +const val POINT_SEPARATOR = '.' +const val COMMA_SEPARATOR = ',' const val DECIMAL_SEPARATOR_LIMIT = 1 @Composable diff --git a/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp new file mode 100644 index 0000000000..bc3845b8b7 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp differ diff --git a/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp new file mode 100644 index 0000000000..c051b16418 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp differ 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 3121a961d7..a92a4f5eb9 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 @@ -23,7 +23,17 @@ internal class DefaultPromoRepository( }.getOrNull() } + override suspend fun getTravalaPromoBanner(): PromoBanner? { + return runCatching(dispatchers.io) { + promoResponseConverter.convert( + tangemApi.getPromotionInfo(TRAVALA) + .getOrThrow(), + ) + }.getOrNull() + } + private companion object { private const val CHANGELLY_NAME = "changelly" + private const val TRAVALA = "travala" } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt index c7ad8f331a..151675506b 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt @@ -13,6 +13,7 @@ class PromoResponseConverter : Converter { name = value.name, bannerState = PromoBanner.BannerState( status = bannerState.status, + link = bannerState.link, timeline = PromoBanner.Timeline( start = DateTime.parse(bannerState.timeline.start), end = DateTime.parse(bannerState.timeline.end), diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt similarity index 55% rename from data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt rename to data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt index 6d9ccf740a..80ba44157d 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt @@ -3,36 +3,48 @@ package com.tangem.data.settings import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow /** * Repository for showing swap promo notification. */ -class DefaultSwapPromoRepository( +class DefaultPromoSettingsRepository( private val appPreferencesStore: AppPreferencesStore, -) : SwapPromoRepository { - override fun isReadyToShowWalletPromo(): Flow { +) : PromoSettingsRepository { + override fun isReadyToShowWalletSwapPromo(): Flow { return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) } - override fun isReadyToShowTokenPromo(): Flow { + override fun isReadyToShowTokenSwapPromo(): Flow { return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) } - override suspend fun setNeverToShowWalletPromo() { + override suspend fun setNeverToShowWalletSwapPromo() { appPreferencesStore.store( key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, value = false, ) } - override suspend fun setNeverToShowTokenPromo() { + override suspend fun setNeverToShowTokenSwapPromo() { appPreferencesStore.store( key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, value = false, ) } + + override fun isReadyToShowWalletTravalaPromo(): Flow { + return appPreferencesStore.get(IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, true) + } + + override suspend fun setNeverToShowWalletTravalaPromo() { + appPreferencesStore.store( + key = IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, + value = false, + ) + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 6211aace4f..4f6bf8e17b 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -2,11 +2,11 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository -import com.tangem.data.settings.DefaultSwapPromoRepository +import com.tangem.data.settings.DefaultPromoSettingsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -31,7 +31,7 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository { - return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore) + fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository { + return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 6797014fd2..615758dcf6 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(deps.tangem.blockchain) /** Core */ + implementation(projects.core.datasource) implementation(projects.core.utils) /** Domain */ @@ -28,4 +29,6 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + implementation(deps.timber) } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 934a88f5c9..96b20e9c0c 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -4,23 +4,26 @@ import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber import java.math.BigDecimal internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, + private val walletManagersStore: WalletManagersStore, private val coroutineDispatcherProvider: CoroutineDispatcherProvider, ) : TransactionRepository { @@ -41,17 +44,54 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) - val txAmount = if (isSwap) { - createAmountForSwap(amount) - } else { - amount - } - return@withContext walletManager?.createTransaction(txAmount, fee, destination)?.copy( + return@withContext walletManager?.createTransactionInternal( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, hash = hash, - extras = getMemoExtras(network.id.value, memo), ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean, + hash: String?, + ): Result { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val validator = walletManager as? TransactionValidator + + return if (validator != null) { + val transaction = walletManager.createTransactionInternal( + amount = amount, + fee = fee ?: Fee.Common(amount = amount), + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, + hash = hash, + ) + + validator.validate(transaction = transaction) + } else { + Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") + Result.success(Unit) + } + } + override suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, @@ -67,6 +107,28 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + @Suppress("LongParameterList") + private fun WalletManager.createTransactionInternal( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + network: Network, + isSwap: Boolean, + hash: String?, + ): TransactionData { + val txAmount = if (isSwap) { + createAmountForSwap(amount) + } else { + amount + } + + return createTransaction(txAmount, fee, destination).copy( + hash = hash, + extras = getMemoExtras(network.id.value, memo), + ) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null @@ -85,7 +147,7 @@ internal class DefaultTransactionRepository( Blockchain.TerraV2, -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index c8949d39c0..a6d7043d91 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.transaction.di import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -20,10 +21,12 @@ internal object TransactionDataModule { @Singleton fun providesTransactionRepository( walletManagersFacade: WalletManagersFacade, + walletManagersStore: WalletManagersStore, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): TransactionRepository { return DefaultTransactionRepository( walletManagersFacade = walletManagersFacade, + walletManagersStore = walletManagersStore, coroutineDispatcherProvider = coroutineDispatcherProvider, ) } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt new file mode 100644 index 0000000000..dc118e8f15 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.card + +import arrow.core.Either + +interface DeleteSavedAccessCodesUseCase { + + suspend operator fun invoke(cardId: String): Either +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 0cbee25c28..56a874bd6a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -30,6 +30,12 @@ interface CardTypesResolver { fun isSatoshiFriendsWallet(): Boolean + fun isBitcoinPizzaDayWallet(): Boolean + + fun isVeChainWallet(): Boolean + + fun isNewWorldEliteWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 077ffe5de1..8c3a6ec485 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -48,6 +48,12 @@ internal class TangemCardTypesResolver( override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID + override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID + + override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID + + override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -154,5 +160,8 @@ internal class TangemCardTypesResolver( const val WHITE_WALLET2_BATCH_ID = "AF15" const val TRILLIANT_WALLET_BATCH_ID = "AF16" const val AVRORA_WALLET_BATCH_ID = "AF18" + const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" + const val VECHAIN_WALLET_BATCH_ID = "AF29" + const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt index 966e1bcf83..8ede57bcf9 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -1,11 +1,11 @@ package com.tangem.domain.settings -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { +class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - operator fun invoke(): Flow = swapPromoRepository.isReadyToShowTokenPromo() + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowTokenSwapPromo() - suspend fun neverToShow() = swapPromoRepository.setNeverToShowTokenPromo() + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowTokenSwapPromo() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt index e2b30e1ba5..233277f71f 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt @@ -1,11 +1,11 @@ package com.tangem.domain.settings -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) { +class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - operator fun invoke(): Flow = swapPromoRepository.isReadyToShowWalletPromo() + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletSwapPromo() - suspend fun neverToShow() = swapPromoRepository.setNeverToShowWalletPromo() + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletSwapPromo() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt new file mode 100644 index 0000000000..643e7387c8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.PromoSettingsRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowTravalaPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { + + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletTravalaPromo() + + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletTravalaPromo() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt new file mode 100644 index 0000000000..0e3946bb89 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.settings.repositories + +import kotlinx.coroutines.flow.Flow + +interface PromoSettingsRepository { + fun isReadyToShowWalletSwapPromo(): Flow + + fun isReadyToShowTokenSwapPromo(): Flow + + suspend fun setNeverToShowWalletSwapPromo() + + suspend fun setNeverToShowTokenSwapPromo() + + fun isReadyToShowWalletTravalaPromo(): Flow + + suspend fun setNeverToShowWalletTravalaPromo() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt deleted file mode 100644 index ca97755e66..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.settings.repositories - -import kotlinx.coroutines.flow.Flow - -interface SwapPromoRepository { - fun isReadyToShowWalletPromo(): Flow - - fun isReadyToShowTokenPromo(): Flow - - suspend fun setNeverToShowWalletPromo() - - suspend fun setNeverToShowTokenPromo() -} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c0a73ff9fd..501cf70dd8 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + implementation(projects.libs.crypto) /** Android - Other */ implementation(deps.androidx.paging.runtime) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt b/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt index 564a49e3d4..2608085143 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt @@ -12,6 +12,7 @@ data class PromoBanner( data class BannerState( val timeline: Timeline, val status: String, + val link: String?, ) data class Timeline( diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 5c1cad5f44..86b0059faf 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -45,4 +45,6 @@ sealed class CryptoCurrencyWarning { val startDateTime: DateTime, val endDateTime: DateTime, ) : CryptoCurrencyWarning() + + data object BeaconChainShutdown : CryptoCurrencyWarning() } \ No newline at end of file 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 43a7a2fda8..5036e8f341 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 @@ -12,6 +12,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import com.tangem.utils.isNullOrZero @@ -74,6 +75,7 @@ class GetCurrencyWarningsUseCase( *coinRelatedWarnings.toTypedArray(), getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), + getBeaconChainShutdownWarning(currency.network.id), ) }.flowOn(dispatchers.io) } @@ -261,6 +263,10 @@ class GetCurrencyWarningsUseCase( } } + private fun getBeaconChainShutdownWarning(networkId: Network.ID): CryptoCurrencyWarning.BeaconChainShutdown? { + return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt new file mode 100644 index 0000000000..0cc768a333 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNetworkAddressesUseCase( + internal val networksRepository: NetworksRepository, +) { + + operator fun invoke(userWalletId: UserWalletId, network: Network): Flow = + networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) + .map { networkStatuses -> + when (val networkStatus = networkStatuses.singleOrNull { it.network.id == network.id }?.value) { + is NetworkStatus.NoAccount -> networkStatus.address.defaultAddress.value + is NetworkStatus.Unreachable -> networkStatus.address?.defaultAddress?.value.orEmpty() + is NetworkStatus.Verified -> networkStatus.address.defaultAddress.value + else -> "" + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt index b15e05db54..ed42ab1cfc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt @@ -5,4 +5,6 @@ import com.tangem.domain.promo.PromoBanner interface PromoRepository { suspend fun getChangellyPromoBanner(): PromoBanner? + + suspend fun getTravalaPromoBanner(): PromoBanner? } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index c28e9dbea7..156e2bb0fa 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -22,6 +22,18 @@ interface TransactionRepository { hash: String?, ): TransactionData? + @Suppress("LongParameterList") + suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + hash: String? = null, + ): Result + suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt new file mode 100644 index 0000000000..56c89954a4 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +class ValidateTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + @Suppress("LongParameterList") + suspend operator fun invoke( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + hash: String? = null, + ): Either { + return transactionRepository.validateTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + isSwap = isSwap, + hash = hash, + ) + .fold(onSuccess = { Unit.right() }, onFailure = { it.left() }) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 76872f50ff..4045a3a98a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull /** * Use case for getting list of user wallets @@ -15,4 +16,7 @@ class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManag @Throws(IllegalArgumentException::class) operator fun invoke(): Flow> = userWalletsListManager.userWallets + + @Throws(IllegalArgumentException::class) + suspend fun invokeSync(): List? = userWalletsListManager.userWallets.firstOrNull() } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 9c8667962c..72d725c460 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -20,10 +20,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningContent @@ -40,10 +39,7 @@ import kotlin.properties.Delegates internal class QrScanningFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: QrScanningRouter diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index b009b1ee67..2ccd4b74fc 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -5,11 +5,10 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel @@ -21,10 +20,7 @@ import javax.inject.Inject class ReferralFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies private val viewModel by viewModels() diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt index d80f08cf56..453a79b60c 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt @@ -7,4 +7,7 @@ interface SendFeatureToggles { /** Availability of redesigned send screen */ val isRedesignedSendEnabled: Boolean + + /** Updates remote toggle */ + suspend fun fetchNewSendEnabled() } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 43a930c199..66e103efad 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.datasource) /** Common */ implementation(projects.common) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt index 6b1465762c..60c8b2822c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt @@ -1,8 +1,10 @@ package com.tangem.features.send.impl.di import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +20,15 @@ internal object SendFeatureTogglesModule { @Provides @Singleton - fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { - return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager) + fun provideSendFeatureToggles( + featureTogglesManager: FeatureTogglesManager, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): SendFeatureToggles { + return DefaultSendFeatureToggles( + featureTogglesManager = featureTogglesManager, + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt index 71f11c480d..b36cba4cbd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt @@ -1,16 +1,41 @@ package com.tangem.features.send.impl.featuretoggles import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber /** * Default implementation of Send feature toggles * * @property featureTogglesManager manager for getting information about the availability of feature toggles + * @property tangemTechApi api to get remote feature toggle for send + * @property dispatchers coroutine dispatchers */ internal class DefaultSendFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, ) : SendFeatureToggles { + + private val remoteSendEnabled: MutableStateFlow = MutableStateFlow(true) + override val isRedesignedSendEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") && + remoteSendEnabled.value + + override suspend fun fetchNewSendEnabled() { + runCatching(dispatchers.io) { + tangemTechApi.getFeatures().getOrThrow() + }.onSuccess { response -> + remoteSendEnabled.update { response.isNewSendEnabled } + }.onFailure { + Timber.e(it.localizedMessage, "Unable to fetch new send toggle") + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 0de48644db..1c7d7f9cee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -6,11 +6,10 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter @@ -27,10 +26,7 @@ import javax.inject.Inject internal class SendFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: SendRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt index feca8a0786..c4ce202599 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt @@ -1,7 +1,9 @@ package com.tangem.features.send.impl.presentation.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE @@ -66,9 +68,9 @@ internal sealed class SendAnalyticEvents( data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") /** Selected fee (send after next screen opened) */ - data class SelectedFee(val feeType: SelectedFeeType) : SendAnalyticEvents( + data class SelectedFee(val feeType: AnalyticsParam.FeeType) : SendAnalyticEvents( event = "Fee Selected", - params = mapOf("Fee Type" to feeType.name), + params = mapOf("Fee Type" to feeType.value), ) /** Custom fee selected */ @@ -97,7 +99,16 @@ internal sealed class SendAnalyticEvents( // region Transaction Result /** Transaction send screen opened */ - data object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened") + data class TransactionScreenOpened( + val token: String, + val feeType: AnalyticsParam.FeeType, + ) : SendAnalyticEvents( + event = "Transaction Sent Screen Opened", + params = mapOf( + TOKEN to token, + FEE_TYPE to feeType.value, + ), + ) /** Share button clicked */ data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share") @@ -145,12 +156,4 @@ internal enum class EnterAddressSource { internal enum class SelectedCurrencyType(val value: String) { Token("Token"), AppCurrency("App Currency"), -} - -internal enum class SelectedFeeType { - Min, - Max, - Fixed, - Normal, - Custom, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt index 27c93b9743..ac6485fc8a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt @@ -2,8 +2,10 @@ package com.tangem.features.send.impl.presentation.analytics.utils import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType -import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.state.SendUiState @@ -11,11 +13,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.utils.Provider internal class SendScreenAnalyticSender( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, + private val cryptoCurrencyProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, ) { fun send(prevScreen: SendUiStateType, state: SendUiState) { @@ -73,16 +77,57 @@ internal class SendScreenAnalyticSender( ) } + fun sendTransaction() { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val cryptoCurrency = cryptoCurrencyProvider() + val feeState = state.getFeeState(isEditState) ?: return + val recipientState = state.getRecipientState(isEditState) ?: return + + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val feeType = getSendTransactionFeeType(feeSelectorState) + analyticsEventHandler.send( + SendAnalyticEvents.TransactionScreenOpened( + token = cryptoCurrency.symbol, + feeType = feeType, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Send( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = feeType, + ), + memoType = getSendTransactionMemoType(recipientState.memoTextField), + ), + ) + } + private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) { - val type = when (feeSelectorState.fees) { - is TransactionFee.Single -> SelectedFeeType.Fixed - is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) { - FeeType.Slow -> SelectedFeeType.Min - FeeType.Market -> SelectedFeeType.Normal - FeeType.Fast -> SelectedFeeType.Max - FeeType.Custom -> SelectedFeeType.Custom - } - } + val type = getSendTransactionFeeType(feeSelectorState) analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type)) } + + private fun getSendTransactionFeeType(feeSelectorState: FeeSelectorState.Content): AnalyticsParam.FeeType = + when (feeSelectorState.fees) { + is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed + is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) { + FeeType.Slow -> AnalyticsParam.FeeType.Min + FeeType.Market -> AnalyticsParam.FeeType.Normal + FeeType.Fast -> AnalyticsParam.FeeType.Max + FeeType.Custom -> AnalyticsParam.FeeType.Custom + } + } + + private fun getSendTransactionMemoType( + recipientMemo: SendTextField.RecipientMemo?, + ): Basic.TransactionSent.MemoType { + val memo = recipientMemo?.value + return when { + memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty + memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full + else -> Basic.TransactionSent.MemoType.Null + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt index 69e56bfc62..8a7d5e7f3a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt @@ -1,15 +1,18 @@ package com.tangem.features.send.impl.presentation.domain import androidx.compose.runtime.Immutable +import com.tangem.domain.wallets.models.UserWalletId /** * Available wallet to send * * @property name wallet name + * @property userWalletId wallet id * @property address blockchain address */ @Immutable data class AvailableWallet( val name: String, + val userWalletId: UserWalletId, val address: String, ) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index a22b3ee620..f2bf0e4d67 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -54,7 +54,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { wrappedList(cryptoCurrency, utxoLimit, amountLimit), ), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_reduce_to, wrappedList(amountLimit)), + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), onClick = onConfirmClick, ), ) @@ -98,7 +98,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_existential_deposit_button, wrappedList(deposit)), + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), onClick = onConfirmClick, ), ) @@ -119,12 +119,13 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) { data class HighFeeError( + val currencyName: String, val amount: String, val onConfirmClick: () -> Unit, val onCloseClick: () -> Unit, ) : Warning( title = resourceReference(R.string.send_notification_high_fee_title), - subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), onClick = onConfirmClick, @@ -156,9 +157,33 @@ internal sealed class SendNotification(val config: NotificationConfig) { data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( title = resourceReference(R.string.send_network_fee_warning_title), subtitle = resourceReference( - R.string.send_network_fee_warning_content, + R.string.common_network_fee_warning_content, wrappedList(cryptoAmount, fiatAmount), ), ) } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index baa881f4d6..377143195d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId @@ -8,6 +9,7 @@ import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase @@ -15,6 +17,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -23,6 +27,7 @@ import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -46,6 +51,7 @@ internal class SendNotificationFactory( private val clickIntents: SendClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, ) { fun create(): Flow> = stateRouterProvider().currentState @@ -66,19 +72,21 @@ internal class SendNotificationFactory( amountValue = amountValue, feeValue = feeValue, ) - val sendingAmount = calculateSubtractedAmount( - isFeeCoverage = isFeeCoverage, + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isFeeCoverage, cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), amountValue = amountValue, feeValue = feeValue, + reduceAmountBy = sendState.reduceAmountBy, ) buildList { // errors addFeeUnreachableNotification(feeState.feeSelectorState) addExceedBalanceNotification(feeValue, sendingAmount) addExceedsBalanceNotification(feeState.fee) - addDustWarningNotification(feeValue, sendingAmount) + addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount) addTransactionLimitErrorNotification(feeValue, sendingAmount) + // warnings addExistentialWarningNotification(feeValue, amountValue) addFeeCoverageNotification( @@ -89,6 +97,9 @@ internal class SendNotificationFactory( addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce) addTooHighNotification(feeState.feeSelectorState) addTooLowNotification(feeState) + + // blockchain specific + addCardanoNotifications(sendingAmount, feeState.fee, state) }.toImmutableList() } @@ -194,7 +205,7 @@ internal class SendNotificationFactory( val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { feeAmount } else { - feeAmount + receivedAmount + receivedAmount } val currencyDeposit = currencyChecksRepository.getExistentialDeposit( userWalletId, @@ -260,6 +271,7 @@ internal class SendNotificationFactory( if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( SendNotification.Warning.HighFeeError( + currencyName = cryptoCurrencyStatus.currency.name, amount = threshold.toPlainString(), onConfirmClick = { clickIntents.onAmountReduceClick( @@ -275,23 +287,35 @@ internal class SendNotificationFactory( } } - private suspend fun MutableList.addDustWarningNotification( - feeAmount: BigDecimal, - receivedAmount: BigDecimal, + private suspend fun MutableList.addDustWarningNotificationForSpecificBlockchains( + feeValue: BigDecimal, + sendingAmount: BigDecimal, ) { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val dustValue = currencyChecksRepository.getDustValue( - userWalletProvider().walletId, - cryptoCurrencyStatus.currency.network, - ) ?: return + val isCardano = BlockchainUtils.isCardano(cryptoCurrencyStatusProvider().currency.network.id.value) - if (checkDustLimits(feeAmount, receivedAmount, dustValue)) { - add( - SendNotification.Error.MinimumAmountError(dustValue.toPlainString()), - ) + if (!isCardano) { + addDustWarningNotification(feeValue, sendingAmount) } } + private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } + private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return @@ -393,13 +417,90 @@ internal class SendNotificationFactory( return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum } - private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + private suspend fun MutableList.addCardanoNotifications( + sendingAmount: BigDecimal, + fee: Fee?, + state: SendUiState, + ) { + val sendingCurrency = cryptoCurrencyStatusProvider().currency + if (!BlockchainUtils.isCardano(sendingCurrency.network.id.value)) return - val totalAmount = feeAmount + receivedAmount - val change = balance - totalAmount - val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO - return receivedAmount < dustValue || isChangeLowerThanDust + validateTransactionUseCase( + amount = sendingAmount.convertToAmount(sendingCurrency), + fee = fee ?: return, + memo = state.recipientState?.memoTextField?.value, + destination = requireNotNull(state.recipientState?.addressTextField?.value), + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ).fold( + ifLeft = { + addCardanoTransactionValidationError( + error = it as? BlockchainSdkError.Cardano ?: return@fold, + sendingCurrency = sendingCurrency, + ) + }, + ifRight = { + (fee as? Fee.CardanoToken)?.let { + add( + SendNotification.Cardano.MinAdaValueCharged( + tokenName = sendingCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + }, + ) + } + + private suspend fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> SendNotification.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue( + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ) ?: return + + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + + private suspend fun MutableList.addDustWarningNotification( + feeValue: BigDecimal, + sendingAmount: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val dustValue = currencyChecksRepository.getDustValue( + userWalletProvider().walletId, + cryptoCurrencyStatus.currency.network, + ) ?: return + + if (checkDustLimits(feeValue, sendingAmount, dustValue)) { + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index b06d42a23d..a8a93bcd32 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -54,7 +54,7 @@ internal fun checkFeeCoverage( /** * Calculates subtracted amount */ -internal fun calculateSubtractedAmount( +private fun calculateSubtractedAmount( isFeeCoverage: Boolean, cryptoCurrencyStatus: CryptoCurrencyStatus, amountValue: BigDecimal, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index a2a13e987a..62bf1460ad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -47,7 +47,7 @@ internal class BitcoinCustomFeeConverter( keyboardType = KeyboardType.Number, ), title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_max_fee_footer), + footer = resourceReference(R.string.send_bitcoin_custom_fee_footer), label = getFiatReference( rate = feeCurrency?.fiatRate, value = feeValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 6bed843250..d64f1c3d47 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -43,7 +43,7 @@ internal class EthereumCustomFeeConverter( keyboardType = KeyboardType.Number, ), title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_max_fee_footer), + footer = resourceReference(R.string.send_evm_custom_fee_footer), label = getFiatReference( rate = feeCurrency?.fiatRate, value = feeValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 7d4dfe3b45..8f3d03dbbc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -10,6 +10,7 @@ import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero +import java.math.RoundingMode internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, @@ -33,7 +34,7 @@ internal class SendAmountFieldMaxAmountConverter( val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() - val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty() + val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() return state.copyWrapped( isEditState = isEditState, amountState = amountState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index e858d9b150..93cc9067fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -26,12 +26,15 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.utils.getFiatFormatted @Composable internal fun SendNavigationButtons( @@ -172,21 +175,27 @@ private fun SendingText( } if (feeFiat != null && sendingFiat != null) { - val sendingValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = sendingFiat, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, + val sendingValue = getFiatFormatted( + value = sendingFiat, + currencySymbol = feeState.appCurrency.symbol, + currencyCode = feeState.appCurrency.code, ) - val feeValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeFiat, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, + val feeValue = getFiatFormatted( + value = feeState.fee?.amount?.value, + currencySymbol = feeState.appCurrency.symbol, + currencyCode = feeState.appCurrency.code, ) + val textResource = remember(sendingValue, feeValue) { + resourceReference( + id = R.string.send_summary_transaction_description, + formatArgs = wrappedList(sendingValue, feeValue), + ) + } Text( - text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), + text = textResource.resolveAnnotatedReference(), textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, modifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index d6529709b7..819655152a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) { val snackbarHostState = remember { SnackbarHostState() } - BackHandler(onBack = uiState.clickIntents::onBackClick) + val onBackClick = uiState.clickIntents::onBackClick.takeIf { + uiState.sendState?.isSending != true + } ?: {} + BackHandler(onBack = onBackClick) Column( modifier = Modifier .fillMaxSize() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt index fcff795b70..929cfdf3b2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt @@ -15,6 +15,7 @@ internal fun LazyListScope.notifications( notifications: ImmutableList, modifier: Modifier = Modifier, hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, ) { itemsIndexed( items = notifications, @@ -44,6 +45,7 @@ internal fun LazyListScope.notifications( -> null is SendNotification.Error -> TangemTheme.colors.icon.warning }, + isEnabled = !isClickDisabled, ) }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index d9c609a5ca..d29fea2394 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -114,11 +115,14 @@ private fun FeeError(feeSelectorState: FeeSelectorState) { private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { val choosableFees = fees as? TransactionFee.Choosable + val decimals = fees.normal.amount.decimals + val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals) + val customAmount = fees.normal.amount.copy(value = customValue) return when (feeType) { FeeType.Slow -> choosableFees?.minimum?.amount FeeType.Market -> fees.normal.amount FeeType.Fast -> choosableFees?.priority?.amount - FeeType.Custom -> null + FeeType.Custom -> customAmount } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 5c0ca33a84..c59617aac0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -33,9 +33,10 @@ internal fun TextFieldWithPaste( ) { val (title, color) = when { isError && error != null -> error to TangemTheme.colors.text.warning - isReadOnly -> label to TangemTheme.colors.text.disabled + isReadOnly -> label to TangemTheme.colors.text.tertiary else -> label to TangemTheme.colors.text.secondary } + val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled FooterContainer(modifier, footer) { Box( modifier = Modifier @@ -59,6 +60,7 @@ internal fun TextFieldWithPaste( SimpleTextField( value = value, placeholder = placeholder, + placeholderColor = placeholderColor, onValueChange = onValueChange, readOnly = isReadOnly, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 3533b2a175..e994f44e6f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -26,7 +26,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.AmountStateP @Composable internal fun AmountBlock( amountState: SendStates.AmountState, - isSuccess: Boolean, + isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit, ) { @@ -55,7 +55,7 @@ internal fun AmountBlock( modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -92,7 +92,7 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::cla TangemThemePreview { AmountBlock( amountState = value, - isSuccess = false, + isClickDisabled = false, isEditingDisabled = false, onClick = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 9e762ae382..68dc4c0c8b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -28,13 +28,13 @@ import com.tangem.features.send.impl.presentation.utils.getCryptoReference import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable -internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { +internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess, onClick = onClick) + .clickable(enabled = !isClickDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -118,7 +118,7 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va TangemThemePreview { FeeBlock( feeState = value, - isSuccess = true, + isClickDisabled = true, onClick = {}, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 0b4752d640..4839b83690 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -25,7 +25,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta @Composable internal fun RecipientBlock( recipientState: SendStates.RecipientState, - isSuccess: Boolean, + isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit, ) { @@ -40,7 +40,7 @@ internal fun RecipientBlock( .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { AddressBlock(recipientState.addressTextField) @@ -107,7 +107,7 @@ private fun RecipientBlockPreview( TangemThemePreview { RecipientBlock( recipientState = value, - isSuccess = true, + isClickDisabled = true, isEditingDisabled = false, onClick = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 6a6cc0550d..6c6de6e979 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -35,12 +35,13 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L @Composable internal fun SendContent(uiState: SendUiState) { val sendState = uiState.sendState ?: return + val isClickDisabled = sendState.isSending || sendState.isSuccess LazyColumn( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) - notifications(sendState.notifications) + notifications(notifications = sendState.notifications, isClickDisabled = isClickDisabled) } } @@ -50,6 +51,7 @@ private fun LazyListScope.blocks(uiState: SendUiState) { val feeState = uiState.feeState ?: return val sendState = uiState.sendState ?: return val isSuccess = sendState.isSuccess + val isClickDisabled = sendState.isSending || isSuccess val timestamp = sendState.transactionDate item(key = BLOCKS_KEY) { @@ -65,19 +67,19 @@ private fun LazyListScope.blocks(uiState: SendUiState) { } RecipientBlock( recipientState = recipientState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, isEditingDisabled = uiState.isEditingDisabled, onClick = uiState.clickIntents::showRecipient, ) AmountBlock( amountState = amountState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, isEditingDisabled = uiState.isEditingDisabled, onClick = uiState.clickIntents::showAmount, ) FeeBlock( feeState = feeState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, onClick = uiState.clickIntents::showFee, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index 73ef044814..f2be7e6ed2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -11,6 +11,7 @@ import java.math.BigDecimal import java.math.RoundingMode private const val FIAT_DECIMALS = 2 +private const val CRYPTO_FEE_DECIMALS = 6 private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { @@ -21,7 +22,7 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex BigDecimalFormatter.formatCryptoAmount( cryptoAmount = amount.value, cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, + decimals = amount.decimals.coerceAtMost(CRYPTO_FEE_DECIMALS), ), ), ) @@ -36,24 +37,27 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) - val scaled = feeValue.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO - val formattedValue = if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { + return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol) +} + +internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String { + val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO + return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { buildString { append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) append( BigDecimalFormatter.formatFiatAmount( fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = currencySymbol, ), ) } } else { BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, + fiatAmount = value, + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = currencySymbol, ) } - return formattedValue } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 5410b54f17..d2231aaf7f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -26,14 +26,10 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet @@ -59,8 +55,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import java.util.Locale @@ -79,8 +76,8 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -95,6 +92,8 @@ internal class SendViewModel @Inject constructor( private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + validateTransactionUseCase: ValidateTransactionUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, @@ -173,6 +172,7 @@ internal class SendViewModel @Inject constructor( clickIntents = this, analyticsEventHandler = analyticsEventHandler, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + validateTransactionUseCase = validateTransactionUseCase, ) private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { @@ -180,6 +180,7 @@ internal class SendViewModel @Inject constructor( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, analyticsEventHandler = analyticsEventHandler, + cryptoCurrencyProvider = Provider { cryptoCurrency }, ) } @@ -188,6 +189,7 @@ internal class SendViewModel @Inject constructor( private set private var userWallet: UserWallet by Delegates.notNull() + private var userWallets: List = emptyList() private var isAmountSubtractAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -205,7 +207,6 @@ internal class SendViewModel @Inject constructor( private var sendIdleTimer = 0L init { - subscribeOnQRScannerResult() subscribeOnCurrencyStatusUpdates() subscribeOnBalanceHidden() getTapHelpPreviewAvailability() @@ -372,7 +373,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = currencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus - + subscribeOnQRScannerResult() when { uiState.sendState?.isSuccess == true -> { stateRouter.showSend() @@ -400,57 +401,48 @@ internal class SendViewModel @Inject constructor( } private fun getUserWallets() { - getWalletsUseCase() - .conflate() - .distinctUntilChanged() - .onEach { userWallets -> - coroutineScope { - runCatching { - userWallets - .filterNot { it.walletId == userWalletId || it.isLocked } - .map { wallet -> - async(dispatchers.io) { wallet.toAvailableWallet() } - }.awaitAll() - }.onSuccess { result -> - uiState = stateFactory.onLoadedWalletsList(wallets = result) - }.onFailure { - uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + viewModelScope.launch(dispatchers.main) { + runCatching { + getWalletsUseCase.invokeSync() + ?.toAvailableWallets() + .orEmpty() + }.onSuccess { result -> + combine(*result.toTypedArray()) { it } + .onEach { + userWallets = it.filterNotNull().toList() + uiState = stateFactory.onLoadedWalletsList(wallets = userWallets) } - } - } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - } - - private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { - return if (!isMultiCurrency) { - val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull() - val address = status?.value?.networkAddress.takeIf { - status?.currency?.network?.id == cryptoCurrency.network.id && - status.currency.network.derivationPath !is Network.DerivationPath.Custom - } - address?.let { - AvailableWallet( - name = name, - address = it.defaultAddress.value, - ) - } - } else { - val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull() - val walletCurrency = statuses?.firstOrNull { - it.currency.network.id == cryptoCurrency.network.id && - it.currency.network.derivationPath !is Network.DerivationPath.Custom - } - val address = walletCurrency?.value?.networkAddress - address?.let { - AvailableWallet( - name = name, - address = it.defaultAddress.value, - ) + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + }.onFailure { + uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) } } } + private suspend fun List.toAvailableWallets(): List> = + filterNot { it.walletId == userWalletId || it.isLocked } + .mapNotNull { wallet -> + val status = if (!wallet.isMultiCurrency) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + if (it.network.id == cryptoCurrency.network.id) { + getNetworkAddressesUseCase(wallet.walletId, it.network) + } else { + null + } + } + } else { + getNetworkAddressesUseCase(wallet.walletId, cryptoCurrency.network) + } + status?.map { address -> + AvailableWallet( + name = wallet.name, + address = address, + userWalletId = wallet.walletId, + ) + } + } + private suspend fun getTxHistory() { val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( userWalletId = userWalletId, @@ -637,7 +629,7 @@ internal class SendViewModel @Inject constructor( }.saveIn(memoValidationJobHolder) } - private suspend fun validateAddress(value: String): Boolean { + private suspend fun validateAddress(value: String): Boolean = runCatching { val isValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, @@ -647,7 +639,7 @@ internal class SendViewModel @Inject constructor( ?.any { it.value == value } ?: true onEnteredValidAddress(isValidAddress, isAddressInWallet) return isValidAddress - } + }.getOrElse { false } private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> @@ -875,11 +867,25 @@ internal class SendViewModel @Inject constructor( uiState = stateFactory.getSendingStateUpdate(isSending = false) updateTransactionStatus(txData) scheduleBalanceUpdate() - analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened) + addTokenToWalletIfNeeded() + sendScreenAnalyticSender.sendTransaction() }, ) } + private fun addTokenToWalletIfNeeded() { + if (cryptoCurrency !is CryptoCurrency.Token) return + + val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return + val destinationAddress = recipientState.addressTextField.value + + val maybeUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return + + viewModelScope.launch(dispatchers.io) { + addCryptoCurrenciesUseCase(userWalletId = maybeUserWallet.userWalletId, currency = cryptoCurrency) + } + } + private suspend fun updateTransactionStatus(txData: TransactionData) { val txUrl = getExplorerTransactionUrlUseCase( userWalletId = userWalletId, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 10ccde5b9f..73348d6d0b 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -47,5 +47,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.timber) implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) implementation(deps.moshi) } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index 318b5635de..a7e59700b2 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -9,4 +9,13 @@ sealed class Warning { data class MinAmountWarning(val dustValue: BigDecimal) : Warning() data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() + + sealed class Cardano : Warning() { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano() + + data object InsufficientBalanceToTransferCoin : Cardano() + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano() + } } \ No newline at end of file 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 8deb74e13e..8bba53e589 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 @@ -5,9 +5,11 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -21,6 +23,8 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.EstimateFeeUseCase @@ -36,15 +40,12 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.AnalyticsData -import com.tangem.lib.crypto.models.ApproveTxData -import com.tangem.lib.crypto.models.ProxyAmount -import com.tangem.lib.crypto.models.ProxyFees +import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.isNullOrZero import com.tangem.utils.toFiatString import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull @@ -73,6 +74,7 @@ internal class SwapInteractorImpl @Inject constructor( private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, private val demoConfig: DemoConfig, + private val transactionRepository: TransactionRepository, ) : SwapInteractor { private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -350,6 +352,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = TxFeeState.Empty, + transactionFee = null, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ) } @@ -379,6 +382,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, + minAdaValue: BigDecimal?, ): List { val fromToken = fromTokenStatus.currency val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() @@ -386,6 +390,13 @@ internal class SwapInteractorImpl @Inject constructor( manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) manageReduceAmountWarning(warnings, fromTokenStatus, amount) + manageCardanoTransactionValidationWarnings( + warnings = warnings, + fromToken = fromToken, + amount = amount, + userWalletId = userWalletId, + minAdaValue = minAdaValue, + ) return warnings } @@ -414,23 +425,35 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, ) { + if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return + val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val dust = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) - val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO - if (dust != null && - !balance.isNullOrZero() && - amount.value < balance - ) { - val change = balance - (amount.value + fee) - val isChangeLowerThanDust = change < dust && change != BigDecimal.ZERO - val isShowWarning = amount.value + fee < dust || isChangeLowerThanDust - if (isShowWarning) { - warnings.add(Warning.MinAmountWarning(dust)) + + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return + + val change = when (fromTokenStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO + balance - (fee + amount.value) } + is CryptoCurrency.Token -> { + val nativeTokenBalance = userWalletManager.getNativeTokenBalance( + fromTokenStatus.currency.network.id.value, + fromTokenStatus.currency.network.derivationPath.value, + ) + + nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + + if (amount.value < dustValue || isChangeLowerThanDust) { + warnings.add(Warning.MinAmountWarning(dustValue)) } } @@ -445,6 +468,72 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun manageCardanoTransactionValidationWarnings( + warnings: MutableList, + fromToken: CryptoCurrency, + amount: SwapAmount, + userWalletId: UserWalletId, + minAdaValue: BigDecimal?, + ) { + transactionRepository.validateTransaction( + amount = amount.value.convertToAmount(fromToken), + fee = null, + memo = null, + destination = getTokenAddress(fromToken), + userWalletId = userWalletId, + network = fromToken.network, + ) + .fold( + onFailure = { + addCardanoTransactionValidationError( + warnings = warnings, + error = it as? BlockchainSdkError.Cardano ?: return@fold, + fromToken = fromToken, + userWalletId = userWalletId, + ) + }, + onSuccess = { + minAdaValue?.let { + warnings.add( + Warning.Cardano.MinAdaValueCharged( + tokenName = fromToken.name, + minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), + ), + ) + } + }, + ) + } + + private suspend fun addCardanoTransactionValidationError( + warnings: MutableList, + error: BlockchainSdkError.Cardano, + fromToken: CryptoCurrency, + userWalletId: UserWalletId, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (fromToken) { + is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + } + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return + + Warning.MinAmountWarning(dustValue) + } + } + .let(warnings::add) // add warning to the list + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -841,8 +930,16 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { + val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + getUnhandledFee( + amount = amount.value, + userWalletId = userWalletId, + cryptoCurrency = fromToken, + ) + } + val txFee = if (provider.type == ExchangeProviderType.CEX) { - getFeeForCex(amount, fromTokenStatus) + getFeeForCex(txFeeResult, fromTokenStatus) } else { TxFeeState.Empty } @@ -881,11 +978,13 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = txFee, + transactionFee = txFeeResult?.getOrNull(), includeFeeInAmount = includeFeeInAmount, ) } } + @Suppress("LongMethod") private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, quoteDataModel: Either, @@ -896,6 +995,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, txFee: TxFeeState, + transactionFee: TransactionFee?, includeFeeInAmount: IncludeFeeInAmount, ): SwapState { return quoteDataModel.fold( @@ -909,7 +1009,12 @@ internal class SwapInteractorImpl @Inject constructor( swapData = null, txFeeState = txFee, ).copy( - warnings = manageWarnings(fromToken, amount, txFee), + warnings = manageWarnings( + fromTokenStatus = fromToken, + amount = amount, + feeState = txFee, + minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, + ), ) when (exchangeProviderType) { @@ -1113,7 +1218,14 @@ internal class SwapInteractorImpl @Inject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings(fromToken, amount, txFeeState), + warnings = manageWarnings( + fromToken, + amount, + txFeeState, + (feeData as? ProxyFees.SingleFee)?.let { + (it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue + }, + ), preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, @@ -1179,23 +1291,26 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun getFeeForCex(amount: SwapAmount, fromToken: CryptoCurrencyStatus): TxFeeState { - getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> - val txFeeResult = estimateFeeUseCase( - amount = amount.value, - userWalletId = userWalletId, - cryptoCurrency = fromToken.currency, - ).firstOrNull() - return txFeeResult?.fold( - ifLeft = { - TxFeeState.Empty - }, - ifRight = { txFee -> - txFee.toTxFeeState(fromToken.currency) - }, - ) ?: TxFeeState.Empty - } - return TxFeeState.Empty + private suspend fun getFeeForCex( + txFeeResult: Either?, + fromToken: CryptoCurrencyStatus, + ): TxFeeState { + return txFeeResult?.fold( + ifLeft = { TxFeeState.Empty }, + ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) }, + ) ?: TxFeeState.Empty + } + + private suspend fun getUnhandledFee( + amount: BigDecimal, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Either? { + return estimateFeeUseCase( + amount = amount, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).firstOrNull() } @Suppress("LongParameterList", "LongMethod") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 9c2290da01..1a6a6e8b22 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -50,6 +50,7 @@ class SwapDomainModule { coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, currenciesRepository: CurrenciesRepository, + transactionRepository: TransactionRepository, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -69,6 +70,7 @@ class SwapDomainModule { currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, demoConfig = DemoConfig(), + transactionRepository = transactionRepository, ) } diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index c78375b5e4..643cf8f201 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.settings) @@ -56,6 +57,9 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) + /** Libs */ + implementation(projects.libs.crypto) + /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.compose.accompanist.webView) @@ -67,5 +71,4 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 74d4f48e01..cd22de2a3b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -110,6 +110,7 @@ sealed interface SwapWarning { val type: GenericWarningType = GenericWarningType.OTHER, val onClick: () -> Unit, ) : SwapWarning + data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning @@ -117,6 +118,16 @@ sealed interface SwapWarning { data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning + + sealed interface Cardano : SwapWarning { + val notificationConfig: NotificationConfig + + data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano + } } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 591d7df1a7..6409755135 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -6,11 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.swap.router.CustomTabsManager import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -26,10 +25,7 @@ import javax.inject.Inject class SwapFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var reduxNavController: ReduxNavController diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b4557850a7..55ede01473 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -216,6 +216,7 @@ internal class StateBuilder( val warnings = getWarningsForSuccessState( quoteModel = quoteModel, fromToken = fromToken, + selectedFeeType = selectedFeeType, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus @@ -314,12 +315,13 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + selectedFeeType: FeeType, ): List { val warnings = mutableListOf() maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) - maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) + maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) maybeAddInsufficientFundsWarning(quoteModel, warnings) maybeAddTransactionInProgressWarning(quoteModel, warnings) @@ -405,6 +407,15 @@ internal class StateBuilder( ), ) } + Warning.Cardano.InsufficientBalanceToTransferCoin -> { + warnings.add(createInsufficientBalanceToTransferCoin()) + } + is Warning.Cardano.InsufficientBalanceToTransferToken -> { + warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName)) + } + is Warning.Cardano.MinAdaValueCharged -> { + warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName)) + } } } } @@ -451,18 +462,37 @@ internal class StateBuilder( private fun maybeAddNetworkFeeCoverageWarning( quoteModel: SwapState.QuotesLoadedState, warnings: MutableList, + selectedFeeType: FeeType, ) { when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> + is IncludeFeeInAmount.Included -> { + val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return warnings.add( SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), + createNetworkFeeCoverageNotificationConfig( + quoteModel.fromTokenInfo.tokenAmount.getFormattedCryptoAmount( + quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency, + ), + fee.feeFiatFormatted, + ), ), ) + } else -> Unit } } + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { + return when (txFeeState) { + TxFeeState.Empty -> null + is TxFeeState.SingleFeeState -> txFeeState.fee + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + } + } + private fun maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, @@ -538,12 +568,12 @@ internal class StateBuilder( if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder val warnings = mutableListOf() warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency)) - if (includeFeeInAmount is IncludeFeeInAmount.Included) { - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), - ), + if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) { + val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig( + fromToken.tokenAmount.getFormattedCryptoAmount(fromToken.cryptoCurrencyStatus.currency), + uiStateHolder.fee.amountFiatFormatted, ) + warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification)) } val providerState = getProviderStateForError( swapProvider = swapProvider, @@ -1393,13 +1423,55 @@ internal class StateBuilder( ) } - private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig { + private fun createNetworkFeeCoverageNotificationConfig( + cryptoAmount: String, + fiatAmount: String, + ): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference(R.string.swapping_network_fee_warning_content), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), iconResId = R.drawable.img_attention_20, ) } + + private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning { + return SwapWarning.Cardano.MinAdaValueCharged( + NotificationConfig( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + iconResId = R.drawable.img_attention_20, + ), + ) + } + + private fun createInsufficientBalanceToTransferCoin(): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferCoin( + NotificationConfig( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + iconResId = R.drawable.img_attention_20, + ), + ) + } + + private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferToken( + NotificationConfig( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + iconResId = R.drawable.img_attention_20, + ), + ) + } // end region private fun getShortAddressValue(fullAddress: String): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 1137f0f264..0cdecf9771 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -391,7 +391,8 @@ private fun SwapWarnings(warnings: List) { }, ) } - else -> {} + is SwapWarning.Cardano -> Notification(config = warning.notificationConfig) + SwapWarning.InsufficientFunds -> Unit } SpacerH8() } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index b5b00b25f0..2ddfba7af0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -6,11 +6,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen @@ -28,10 +27,7 @@ import javax.inject.Inject internal class TesterActivity : ComposeActivity() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies /** Router for inner feature navigation */ @Inject diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index d1600bfa93..94200fbd59 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -4,11 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel @@ -20,10 +19,7 @@ import javax.inject.Inject internal class TokenDetailsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter 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 9236e01d5e..b458c3f622 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 @@ -41,6 +41,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, + is TokenDetailsNotification.NetworkShutdown, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index bed8a2a0e2..853191b4a2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -155,7 +155,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - class NetworksNoAccount(val network: String, val symbol: String, val amount: String) : Informational( + data class NetworksNoAccount( + private val network: String, + private val symbol: String, + private val amount: String, + ) : Informational( title = resourceReference(R.string.warning_no_account_title), subtitle = resourceReference( id = R.string.no_account_generic, @@ -167,4 +171,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.warning_no_account_title), subtitle = resourceReference(id = R.string.no_account_send_to_create), ) + + data class NetworkShutdown(private val title: TextReference, private val subtitle: TextReference) : Warning( + title = title, + subtitle = subtitle, + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 01535ea1b4..8f368bb1b3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -3,12 +3,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning 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.components.TokenDetailsNotification.* import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList @@ -80,6 +82,10 @@ internal class TokenDetailsNotificationConverter( onSwapClick = clickIntents::onSwapPromoClick, onCloseClick = clickIntents::onSwapPromoDismiss, ) + is CryptoCurrencyWarning.BeaconChainShutdown -> NetworkShutdown( + title = resourceReference(R.string.warning_beacon_chain_retirement_title), + subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content), + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 3434ccab80..5a2c923015 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -2,11 +2,10 @@ package com.tangem.feature.wallet.presentation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter @@ -22,10 +21,7 @@ import javax.inject.Inject internal class WalletFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - override lateinit var hapticManager: HapticManager + override lateinit var uiDependencies: UiDependencies @Inject internal lateinit var manageTokensUi: ManageTokensUi diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index de6ddf8b1b..f39a38511b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -121,4 +121,38 @@ sealed class WalletScreenAnalyticsEvent { data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } + + sealed class Promotion( + event: String, + params: Map = mapOf(), + ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { + class NoticePromotionBanner( + source: AnalyticsParam.ScreensSources, + programName: String, + ) : Promotion( + event = "Notice - Promotion Banner", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName, + ), + ) + + class PromotionBannerClicked( + source: AnalyticsParam.ScreensSources, + programName: String, + action: BannerAction, + ) : Promotion( + event = "Promo Banner Clicked", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName, + "Action" to action.action, + ), + ) { + sealed class BannerAction(val action: String) { + data object Clicked : BannerAction(action = "Clicked") + data object Closed : BannerAction(action = "Closed") + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 23f6a0c60d..f0ca57a2b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -33,7 +33,7 @@ internal class TokenListAnalyticsSender @Inject constructor( private val mutex = Mutex() suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return if (tokenList.totalFiatBalance is TokenList.FiatBalance.Loading) return 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 e80fc87a24..59d4331387 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 @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent 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 @@ -16,7 +18,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( ) { fun send(displayedUiState: WalletState?, newWarnings: List) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (newWarnings.isEmpty()) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return @@ -44,6 +46,10 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError + is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + ) is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, 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 51fbfe6742..a2adf6b292 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 @@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase +import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, - private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, + private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, @@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) } + val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) } return combine( flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), - flow4 = shouldShowSwapPromoWalletUseCase().conflate(), - flow5 = promoFlow, - ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner -> + flow4 = shouldShowTravalaPromoWalletUseCase().conflate(), + flow5 = travalaPromoFlow.conflate(), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner -> readyForRateAppNotification = true buildList { - addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) + addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents) addCriticalNotifications(userWallet) @@ -69,16 +69,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addSwapPromoNotification( + private fun MutableList.addTravalaPromoNotification( shouldShowPromo: Boolean, promoBanner: PromoBanner?, clickIntents: WalletClickIntents, ) { promoBanner ?: return - val promoNotification = WalletNotification.SwapPromo( + val promoNotification = WalletNotification.TravalaPromo( startDateTime = promoBanner.bannerState.timeline.start, endDateTime = promoBanner.bannerState.timeline.end, - onCloseClick = clickIntents::onCloseSwapPromoClick, + bannerLink = promoBanner.bannerState.link, + onBookNowButtonClick = clickIntents::onTravalaPromoClick, + onCloseClick = clickIntents::onCloseTravalaPromoClick, ) addIf( element = promoNotification, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 7c7adbde20..3613a6439a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -34,6 +34,9 @@ internal object WalletImageResolver { cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() + cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() + cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() + cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -130,6 +133,27 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveBitcoinPizzaDayWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_pizza_day_card2_120_106, + twoBackupResId = R.drawable.ill_pizza_day_card3_120_106, + ) + } + + private fun UserWallet.resolveVeChainWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_vechain_card2_120_106, + twoBackupResId = R.drawable.ill_vechain_card3_120_106, + ) + } + + private fun UserWallet.resolveNewWorldEliteWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_nwe_card2_120_106, + twoBackupResId = R.drawable.ill_nwe_card3_120_106, + ) + } + private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { 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 9f7f5ea139..c34e986c29 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 @@ -2,10 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -173,6 +171,34 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) + data class TravalaPromo( + val startDateTime: DateTime, + val endDateTime: DateTime, + val bannerLink: String?, + val onBookNowButtonClick: (String?) -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.main_travala_promotion_title), + subtitle = resourceReference( + id = R.string.main_travala_promotion_description, + wrappedList( + DateTimeFormatters.formatDate(startDateTime, DateTimeFormatters.dateMMMMd), + DateTimeFormatters.formatDate(endDateTime, DateTimeFormatters.dateMMMMd), + ), + ), + // Stub. Travala has its own Composable implementation with correct img + iconResId = R.drawable.ic_star_24, + // Stub. Travala has its own Composable implementation with correct img + backgroundResId = R.drawable.ic_star_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + onClick = { onBookNowButtonClick(bannerLink) }, + text = resourceReference(R.string.main_travala_promotion_button), + ), + onCloseClick = onCloseClick, + ), + ) + data class SwapPromo( val startDateTime: DateTime, val endDateTime: DateTime, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt index ca91846374..8bf0e4ea63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt @@ -6,4 +6,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model * @property isRefreshing state is indicator visible * @property onRefresh lambda be invoked when pulled to refresh */ -data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file +data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: (ShowRefreshState) -> Unit) { + + @JvmInline + value class ShowRefreshState( + val value: Boolean, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 2dc37ce327..71f0bdd4c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -72,7 +72,7 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn } private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + return WalletPullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index cfeccc9db0..670f85b03a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -135,7 +135,9 @@ private fun WalletContent( .padding(horizontal = horizontalPadding) LazyColumn( - modifier = Modifier.fillMaxSize().testTag(TestTags.WALLET_SCREEN), + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.WALLET_SCREEN), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing92, @@ -317,7 +319,9 @@ private fun BaseScaffoldManageTokenRedesign( content = { paddingValues -> val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Column( @@ -503,7 +507,9 @@ private fun BaseScaffold( content = { val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Box( 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 476f3701e4..6b783e0eb0 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationWithBackground +import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -25,23 +26,33 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning - is WalletNotification.Informational -> TangemTheme.colors.icon.accent - is WalletNotification.RateApp -> TangemTheme.colors.icon.attention - is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 - else -> null - }, - ) + // TODO develop promo banner general component + when (it) { + is WalletNotification.SwapPromo -> { + NotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } + is WalletNotification.TravalaPromo -> { + TravalaNotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } + else -> { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + iconTint = when (it) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + else -> null + }, + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt index 5ebccf6c11..f2c721ac87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt @@ -3,19 +3,21 @@ package com.tangem.feature.wallet.presentation.wallet.utils import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @ViewModelScoped internal class ScreenLifecycleProvider @Inject constructor() : DefaultLifecycleObserver { - var isBackground: Boolean = true - private set + private val _isBackgroundState = MutableStateFlow(false) + val isBackgroundState: StateFlow = _isBackgroundState override fun onResume(owner: LifecycleOwner) { - isBackground = false + _isBackgroundState.value = false } override fun onPause(owner: LifecycleOwner) { - isBackground = true + _isBackgroundState.value = true } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e74ecbdb0b..775bb89a39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -20,6 +20,7 @@ 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.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender @@ -31,7 +32,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,7 +62,9 @@ internal class WalletViewModel @Inject constructor( val uiState: StateFlow = stateHolder.uiState private lateinit var router: InnerWalletRouter - private var walletsUpdateJobHolder: JobHolder = JobHolder() + private val walletsUpdateJobHolder = JobHolder() + private val refreshWalletJobHolder = JobHolder() + private var needToRefreshWallet = false init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) @@ -71,6 +74,7 @@ internal class WalletViewModel @Inject constructor( subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() + subscribeToScreenBackgroundState() } fun setWalletRouter(router: InnerWalletRouter) { @@ -150,6 +154,36 @@ internal class WalletViewModel @Inject constructor( } } + // We need to update the current wallet if the application was in the background for more than 10 seconds + // and then returned to the foreground + private fun subscribeToScreenBackgroundState() { + screenLifecycleProvider.isBackgroundState + .onEach { isBackground -> + refreshWalletJobHolder.cancel() + when { + isBackground -> needToRefreshTimer() + needToRefreshWallet && !isBackground -> triggerRefreshWallet() + } + } + .launchIn(viewModelScope) + } + + private fun needToRefreshTimer() { + viewModelScope.launch { + delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) + needToRefreshWallet = true + }.saveIn(refreshWalletJobHolder) + } + + private fun triggerRefreshWallet() { + needToRefreshWallet = false + val state = stateHolder.uiState.value + val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return + wallet.pullToRefreshConfig.onRefresh.invoke( + WalletPullToRefreshConfig.ShowRefreshState(false), + ) + } + private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) @@ -280,7 +314,7 @@ internal class WalletViewModel @Inject constructor( } private fun closeScreen(screen: AppScreen) { - if (!screenLifecycleProvider.isBackground) { + if (!screenLifecycleProvider.isBackgroundState.value) { stateHolder.clear() router.popBackStack(screen = screen) } @@ -296,4 +330,8 @@ internal class WalletViewModel @Inject constructor( ), ) } + + private companion object { + const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 7e47a2a490..10d52a0047 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,8 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader @@ -32,9 +36,13 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, + private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { @@ -72,7 +80,18 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) + + val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + + deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId) + .onLeft { Timber.e(it.toString()) } + deleteWalletUseCase(userWalletId) + .onRight { + getSelectedWalletSyncUseCase().getOrNull()?.let { + reduxStateHolder.onUserWalletSelected(it) + } + } .onLeft { Timber.e(it.toString()) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 3cf99441ab..3266406422 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -77,17 +77,17 @@ internal class WalletClickIntents @Inject constructor( } } - fun onRefreshSwipe() { + fun onRefreshSwipe(showRefreshState: Boolean) { when (stateHolder.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshMultiCurrencyContent() + refreshMultiCurrencyContent(showRefreshState) } is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState) } is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, @@ -97,14 +97,14 @@ internal class WalletClickIntents @Inject constructor( } fun onReloadClick() { - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState = true) } - private fun refreshMultiCurrencyContent() { + private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { @@ -126,11 +126,11 @@ internal class WalletClickIntents @Inject constructor( // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() - private fun refreshSingleCurrencyContent() { + private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 553dafc78b..418a4d8afd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -11,6 +11,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase +import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -19,6 +20,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler @@ -56,6 +58,10 @@ internal interface WalletWarningsClickIntents { fun onCloseRateAppWarningClick() fun onCloseSwapPromoClick() + + fun onTravalaPromoClick(link: String?) + + fun onCloseTravalaPromoClick() } @Suppress("LongParameterList") @@ -75,6 +81,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, + private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -212,6 +219,34 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onTravalaPromoClick(link: String?) { + analyticsEventHandler.send( + WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked, + ), + ) + link?.let { + viewModelScope.launch(dispatchers.main) { + router.openUrl(link) + } + } + } + + override fun onCloseTravalaPromoClick() { + analyticsEventHandler.send( + WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed, + ), + ) + viewModelScope.launch(dispatchers.main) { + shouldShowTravalaPromoWalletUseCase.neverToShow() + } + } + private suspend fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp new file mode 100644 index 0000000000..b54656293e Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp new file mode 100644 index 0000000000..feecda86b3 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp new file mode 100644 index 0000000000..2608db4c6e Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp new file mode 100644 index 0000000000..d47e557121 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp new file mode 100644 index 0000000000..bb5f23d301 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp new file mode 100644 index 0000000000..249477b20f Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6e2f105776..7c24809756 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -83,12 +83,13 @@ web3j = "4.10.1" leakcanary = "2.13" decompose = "2.2.2" room = "2.6.1" +markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-621" +tangemBlockchainSdk = "develop-633" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-351" +tangemCardSdk = "develop-354" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -258,4 +259,5 @@ decompose-ext-compose = { module = "com.arkivanov.decompose:extensions-compose-j room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } # endregion Other diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 88d96a0405..f9c7b8a359 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { // region Core modules implementation(projects.core.datasource) + implementation(projects.core.featuretoggles) implementation(projects.core.utils) // endregion diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 406be2d877..7adfd7b9e1 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -13,8 +13,10 @@ import com.tangem.datasource.config.models.ConfigValueModel import com.tangem.datasource.config.models.ProviderModel import com.tangem.libs.blockchain_sdk.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber internal typealias BlockchainProvidersResponse = Map> @@ -57,6 +59,7 @@ internal class DefaultBlockchainSDKFactory( return combine( flow = configStore.get(), flow2 = blockchainProviderTypesStore.get(), + // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index feae18a4e4..9ede47a471 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -1,10 +1,12 @@ package com.tangem.blockchainsdk import com.tangem.blockchain.common.AccountCreator +import com.tangem.blockchain.common.BlockchainFeatureToggles import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles import timber.log.Timber import javax.inject.Inject @@ -21,6 +23,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, + private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -30,6 +33,9 @@ internal class WalletManagerFactoryCreator @Inject constructor( config = config, blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, + featureToggles = BlockchainFeatureToggles( + isCardanoTokenSupport = blockchainSDKFeatureToggles.isCardanoTokensSupportEnabled, + ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index ff8c190e0e..7fd86c9aa1 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -37,6 +37,7 @@ internal object BlockchainSDKConfigConverter : Converter