Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-17 14:05:26 +05:00
commit 63849d3c02
155 changed files with 2317 additions and 607 deletions

@ -1 +1 @@
Subproject commit 07cfce37ff84e70081aca82d9948acb13c5c07b0
Subproject commit a494a9b49335d891dc5836d478c843432a857621

View file

@ -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"

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -346,4 +346,12 @@ internal object TokensDomainModule {
): RunPolkadotAccountHealthCheckUseCase {
return RunPolkadotAccountHealthCheckUseCase(repository)
}
@Provides
@ViewModelScoped
fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase {
return GetNetworkAddressesUseCase(
networksRepository = networksRepository,
)
}
}

View file

@ -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)
}
}

View file

@ -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<Throwable, Unit> {
tangemSdkManager.deleteSavedUserCodes(setOf(cardId))
.doOnFailure { return it.left() }
.doOnSuccess { return Unit.right() }
return Unit.right()
}
}

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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<DetailsSta
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {

View file

@ -6,9 +6,8 @@ import androidx.compose.ui.Modifier
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.email.EmailSender
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.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -24,10 +23,7 @@ import javax.inject.Inject
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var walletsRepository: WalletsRepository

View file

@ -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<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
private val viewModel = ResetCardViewModel(store)

View file

@ -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<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
private val viewModel = SecurityModeViewModel(store)

View file

@ -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<WalletConnectState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
private val viewModel: WalletConnectViewModel by viewModels()

View file

@ -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<HomeState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
override lateinit var hapticManager: HapticManager
override lateinit var uiDependencies: UiDependencies
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)

View file

@ -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)
}

View file

@ -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()

View file

@ -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

View file

@ -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 {

View file

@ -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) {

View file

@ -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()

View file

@ -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,

View file

@ -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)
}

View file

@ -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)

View file

@ -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<UserTokensAccountResponse>
@GET("features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
}

View file

@ -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,
)

View file

@ -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,

View file

@ -41,6 +41,7 @@ class ConfigValueModel(
val chiaTangemApiKey: String?,
val devExpress: ExpressModel?,
val express: ExpressModel?,
val polygonScanApiKey: String?,
)
@JsonClass(generateAdapter = true)

View file

@ -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") }

View file

@ -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"
}
]

View file

@ -4,16 +4,12 @@
<string name="add_tokens_title">Валюты</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="alert_button_request_support">Обратиться в поддержку</string>
<string name="alert_button_try_again">Попробовать снова</string>
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
<string name="alert_manage_tokens_unsupported_curve_message">Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.</string>
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="alert_negative_app_rate_sent_message">Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки.</string>
<string name="alert_negative_app_rate_sent_title">Ваши предложения отправлены</string>
<string name="alert_troubleshooting_scan_card_message">Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
@ -28,8 +24,6 @@
<string name="app_settings_theme_mode_dark">Тёмная</string>
<string name="app_settings_theme_mode_light">Светлая</string>
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selection_footer">При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства</string>
<string name="app_settings_theme_selection_system_short">Системная</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
@ -57,10 +51,12 @@
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
<string name="card_settings_security_mode">Тип безопасности</string>
<string name="card_settings_title">Настройки карты</string>
<string name="cardano_coin_will_be_send_with_token_description">Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s</string>
<string name="cardano_insufficient_balance_to_send_token_description">Чтобы совершить транзакцию %1$s, вы должны внести некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA. (рекомендуется баланс в размере 5 ADA)</string>
<string name="cardano_insufficient_balance_to_send_token_title">Недостаточно ADA для отправки токена</string>
<string name="cardano_max_amount_has_token_description">Сеть Cardano не позволяет вам оставлять на своем балансе сумму ниже минимального значения %1$s, если у вас есть токены. Пожалуйста, убедитесь, что у вас осталось немного ADA.</string>
<string name="cardano_coin_will_be_send_with_token_description">Помимо сетевой комиссий, сеть Cardano взимает %1$s ADA при транзакции с токеном %2$s</string>
<string name="cardano_coin_will_be_send_with_token_title">Требования к транзакции Cardano</string>
<string name="cardano_insufficient_balance_to_send_token_description">Чтобы совершить транзакцию %1$s, внесите некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA (рекомендуется 5 ADA)</string>
<string name="cardano_insufficient_balance_to_send_token_title">Недостаточно ADA для транзакции</string>
<string name="cardano_max_amount_has_token_description">Вы должны поддерживать некоторое количество ADA, поскольку у вас на балансе есть токены в сети Cardano</string>
<string name="cardano_max_amount_has_token_title">Недостаточно ADA</string>
<string name="common_accept">Принять</string>
<string name="common_access_denied">Доступ запрещен</string>
<string name="common_apply">Применить</string>
@ -81,7 +77,6 @@
<string name="common_create">Создать</string>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_disconnect">Отключить</string>
<string name="common_done">Готово</string>
<string name="common_enable">Включить</string>
<string name="common_enabled">Включено</string>
@ -102,6 +97,7 @@
<string name="common_locked">Заблокирован</string>
<string name="common_main_network">Основная сеть</string>
<string name="common_network_fee_title">Сетевая комиссия</string>
<string name="common_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии</string>
<string name="common_next">Далее</string>
<string name="common_no">Нет</string>
<string name="common_no_address">Нет адреса</string>
@ -114,7 +110,6 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_retry">Повторить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -138,7 +133,6 @@
<string name="common_understand">Я понял</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_warning">Предупреждение</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
@ -185,7 +179,6 @@
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
<string name="details_row_title_issuer">Эмитент</string>
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_security_management_warning">Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно.</string>
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
@ -267,6 +260,9 @@
<string name="main_swap_changelly_promotion_message">Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля.</string>
<string name="main_swap_changelly_promotion_title">Обмен с Changelly, %s комиссии</string>
<string name="main_tokens">Токены</string>
<string name="main_travala_promotion_button">Забронировать</string>
<string name="main_travala_promotion_description" formatted="false">Оплатите его криптой и сэкономьте **50 долларов** через нашего партнера Travala: **%1s - %2s**</string>
<string name="main_travala_promotion_title">Забронируйте отпуск с Tangem</string>
<string name="manage_tokens_add">Добавить</string>
<string name="manage_tokens_edit">Изменить</string>
<string name="manage_tokens_list_header_title">Рыночная капитализация</string>
@ -445,11 +441,13 @@
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_amount_label">Сумма</string>
<string name="send_bitcoin_custom_fee_footer">Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte.</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_evm_custom_fee_footer">Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
@ -468,26 +466,23 @@
<string name="send_max_amount">Всё</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_fee">Комиссия не превысит </string>
<string name="send_max_fee_footer">Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.</string>
<string name="send_memo_destination_tag_error">Допустим ввод только цифр</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_existential_deposit_button">Оставить %s</string>
<string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе.</string>
<string name="send_notification_existential_deposit_title">Экзистенциальный депозит</string>
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети Tezos комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s.</string>
<string name="send_notification_high_fee_title">Комиссия повышена</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %s</string>
<string name="send_notification_leave_button">Оставить %s</string>
<string name="send_notification_reduce_by">Уменьшить на %s</string>
<string name="send_notification_reduce_to">Уменьшить до %s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
@ -497,7 +492,7 @@
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient">Получатель</string>
<string name="send_recipient_address_error">Неверный адрес</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов.</string>
<string name="send_recipient_label">Отправить</string>
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
@ -506,7 +501,7 @@
<string name="send_sending">Отправка</string>
<string name="send_summary_tap_hint">Нажмите на любое поле, чтобы изменить его</string>
<string name="send_summary_title">Отправка %s</string>
<string name="send_summary_transaction_description">Вы отправляете %1$s, включая комиссию сети %2$s</string>
<string name="send_summary_transaction_description">Вы отправляете **%1$s**, включая комиссию сети %2$s</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
@ -535,7 +530,6 @@
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="swapping_permission_buttons_approve">Подтвердить</string>
<string name="swapping_permission_current_transaction">Текущая транзакция</string>
<string name="swapping_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
@ -560,7 +554,6 @@
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Отправка средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_details_choose_address">Выберите адрес</string>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
@ -568,7 +561,7 @@
<string name="token_details_hide_token">Скрыть токен</string>
<string name="token_details_token_type_subtitle">%1$s токен в сети %%image%% %2$s</string>
<string name="token_details_token_type_subtitle_no_standard">Токен в сети %%image%% %1$s</string>
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="token_details_unable_hide_alert_message">Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_swap_changelly_promotion_message">Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.</string>
<string name="token_swap_changelly_promotion_title">Обмен с Changelly, %s комиссии</string>
@ -597,6 +590,7 @@
<string name="user_wallet_list_delete_prompt">Вы уверены, что хотите удалить этот кошелек?</string>
<string name="user_wallet_list_error_unable_to_unlock">Произошла ошибка, пожалуйста, отсканируйте свою карту для входа</string>
<string name="user_wallet_list_error_wallet_already_saved">Этот кошелек уже был сохранен, вы можете добавить другой</string>
<string name="user_wallet_list_rename_popup_error_already_exists">Кошелек с именем %s уже существует</string>
<string name="user_wallet_list_rename_popup_placeholder">Имя кошелька</string>
<string name="user_wallet_list_rename_popup_title">Переименование кошелька</string>
<string name="user_wallet_list_unlock_all">Разблокировать все</string>
@ -643,6 +637,8 @@
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string>
<string name="warning_backup_errors_title">Ошибка активации</string>
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_title">Отключение сети BNB Beacon Chain</string>
<string name="warning_button_could_be_better">Можно лучше</string>
<string name="warning_button_like_it">Нравится</string>
<string name="warning_button_ok">Понятно!</string>
@ -698,7 +694,6 @@
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
<string name="warning_send_blocked_pending_transactions_title">Транзакция в обработке</string>
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>
<string name="warning_solana_fee_title">Оповещение сети Солана</string>
<string name="warning_solana_rent_fee_message">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>

View file

@ -4,16 +4,12 @@
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
<string name="alert_manage_tokens_unsupported_curve_message">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.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
<string name="alert_negative_app_rate_sent_message">Thank you for your feedback. We will respond as soon as possible</string>
<string name="alert_negative_app_rate_sent_title">Your suggestions were sent</string>
<string name="alert_troubleshooting_scan_card_message">Please try to tap the card exactly as shown in the animation or request support.</string>
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
<string name="app_settings_default_fee">Default Fee</string>
@ -29,8 +25,6 @@
<string name="app_settings_theme_mode_dark">Dark</string>
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selection_footer">If system is selected, the app will auto-adjust based on your device\'s system settings</string>
<string name="app_settings_theme_selection_system_short">System</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
@ -56,10 +50,12 @@
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_security_mode">Security Mode</string>
<string name="card_settings_title">Card Settings</string>
<string name="cardano_coin_will_be_send_with_token_description">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</string>
<string name="cardano_insufficient_balance_to_send_token_description">To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value. (5 ADA balance recommended)</string>
<string name="cardano_insufficient_balance_to_send_token_title">Insufficient ADA to token transfer</string>
<string name="cardano_max_amount_has_token_description">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.</string>
<string name="cardano_coin_will_be_send_with_token_description">In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token</string>
<string name="cardano_coin_will_be_send_with_token_title">Cardano transaction requirements</string>
<string name="cardano_insufficient_balance_to_send_token_description">To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended)</string>
<string name="cardano_insufficient_balance_to_send_token_title">Insufficient ADA for token transfer</string>
<string name="cardano_max_amount_has_token_description">You must maintain some ADA because you have some tokens on the Cardano blockchain</string>
<string name="cardano_max_amount_has_token_title">Not enough ADA</string>
<string name="common_accept">Accept</string>
<string name="common_access_denied">Access denied</string>
<string name="common_apply">Apply</string>
@ -80,7 +76,6 @@
<string name="common_create">Create</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
@ -101,6 +96,7 @@
<string name="common_locked">Locked</string>
<string name="common_main_network">Main network</string>
<string name="common_network_fee_title">Network fee</string>
<string name="common_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
<string name="common_next">Next</string>
<string name="common_no">No</string>
<string name="common_no_address">No address</string>
@ -113,7 +109,6 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_retry">Retry</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -137,7 +132,6 @@
<string name="common_understand">I understand</string>
<string name="common_unknown_error">There was an error. Please try again.</string>
<string name="common_unreachable">Unreachable</string>
<string name="common_warning">Warning</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="currency_subtitle_expanded">Available networks</string>
@ -184,7 +178,6 @@
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_security_management_warning">If you forget the code you will lose access to your funds. Code recovery is not possible.</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of Service</string>
@ -267,6 +260,9 @@
<string name="main_swap_changelly_promotion_message">Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s</string>
<string name="main_swap_changelly_promotion_title">Swap with Changelly, %s fees</string>
<string name="main_tokens">Tokens</string>
<string name="main_travala_promotion_button">Book now</string>
<string name="main_travala_promotion_description" formatted="false">Save **$50** while booking via our partner Travala: **%1s - %2s**</string>
<string name="main_travala_promotion_title">Book your holidays with Tangem and pay in crypto</string>
<string name="manage_tokens_add">Add</string>
<string name="manage_tokens_edit">Edit</string>
<string name="manage_tokens_list_header_title">Coin market cap</string>
@ -442,11 +438,13 @@
<string name="send_amount_label">Amount</string>
<string name="send_base_fee">Base fee</string>
<string name="send_base_fee_footer">Represents the part of the transaction fee that goes to the miner</string>
<string name="send_bitcoin_custom_fee_footer">You can set your transaction fee by adjusting the value in the Satoshi per vByte field.</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_evm_custom_fee_footer">The fee that will be charged for your transaction. You can set your own value.</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
@ -465,26 +463,23 @@
<string name="send_max_amount">Max</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_max_fee">Max fee</string>
<string name="send_max_fee_footer">The fee that will be charged for your transaction. You can set your own value.</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="send_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_existential_deposit_button">Leave %s</string>
<string name="send_notification_existential_deposit_text">The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_high_fee_text">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.</string>
<string name="send_notification_high_fee_text">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.</string>
<string name="send_notification_high_fee_title">The fee is higher</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string>
<string name="send_notification_leave_button">Leave %s</string>
<string name="send_notification_reduce_by">Reduce by %s</string>
<string name="send_notification_reduce_to">Reduce to %s</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_notification_transaction_limit_text">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.</string>
@ -496,7 +491,7 @@
<string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens</string>
<string name="send_recipient_address_footer">Ensure the receiving wallet address is on the %s network to avoid losing your tokens</string>
<string name="send_recipient_label">Send to</string>
<string name="send_recipient_memo_footer">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</string>
<string name="send_recipient_wallets_title">My wallets</string>
@ -505,7 +500,7 @@
<string name="send_sending">Sending...</string>
<string name="send_summary_tap_hint">Tap any field to change it</string>
<string name="send_summary_title">Send %s</string>
<string name="send_summary_transaction_description">You are sending %1$s including a network fee of %2$s</string>
<string name="send_summary_transaction_description">You are sending **%1$s** including a network fee of %2$s</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -535,7 +530,6 @@
<string name="swapping_give_permission">Give Permission</string>
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_permission_current_transaction">Current transaction</string>
<string name="swapping_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
@ -560,7 +554,6 @@
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Sending funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not available at the moment. Please check our updates.</string>
<string name="token_details_choose_address">Choose address</string>
<string name="token_details_generate_xpub">Generate XPUB</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
@ -568,7 +561,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
<string name="token_details_unable_hide_alert_message">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.</string>
<string name="token_details_unable_hide_alert_message">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</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
<string name="token_swap_changelly_promotion_title">Swap with Changelly, %s fees</string>
@ -597,6 +590,7 @@
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
<string name="user_wallet_list_error_unable_to_unlock">An error has occurred, please scan your card to log in</string>
<string name="user_wallet_list_error_wallet_already_saved">This wallet has already been saved, you can add another one</string>
<string name="user_wallet_list_rename_popup_error_already_exists">The wallet with name %s already exists</string>
<string name="user_wallet_list_rename_popup_placeholder">Wallet name</string>
<string name="user_wallet_list_rename_popup_title">Rename Wallet</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
@ -643,6 +637,8 @@
<string name="warning_access_denied_message">Use %s or scan a card to unlock access to your wallet</string>
<string name="warning_backup_errors_message">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.</string>
<string name="warning_backup_errors_title">Activation error</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain will shut down</string>
<string name="warning_button_could_be_better">Could be better</string>
<string name="warning_button_like_it">Like it</string>
<string name="warning_button_ok">Ok, Got it!</string>
@ -674,7 +670,7 @@
<string name="warning_hedera_missing_token_association_message">This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">This token must be associated with your Hedera account before you can receive it</string>
<string name="warning_hedera_missing_token_association_title">Associate your token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Hot enough %s. Top up your Hedera account to associate this token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Not enough %s. Top up your Hedera account to associate this token</string>
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
@ -696,7 +692,6 @@
<string name="warning_rent_fee_title">Network rent fee required</string>
<string name="warning_send_blocked_funds_for_fee_message">%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.</string>
<string name="warning_send_blocked_funds_for_fee_title">Insufficient %1$s to cover network fee</string>
<string name="warning_send_blocked_pending_transactions_title">Transaction pending</string>
<string name="warning_solana_fee_message">The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.</string>
<string name="warning_solana_fee_title">Solana Network Alert</string>
<string name="warning_solana_rent_fee_message">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.</string>

View file

@ -46,4 +46,5 @@ dependencies {
implementation(deps.zxing.qrCore)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.markdown)
}

View file

@ -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
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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,
),
) {

View file

@ -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()

View file

@ -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 <a href="https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=11690-12057&mode=design&t=eFnsA9sNytcQIoQ4-4">Travala Promo</a>
*/
@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

View file

@ -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 <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
*
[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 <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
*
[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<TextReference>(
collection = listOf(
stringReference(value = "Copied!"),
stringReference(value = "Contract address copied!"),
stringReference(value = "Coooooooooooontract addreeeeeeeeeeeeeeeess coooooooooooooooopied!"),
),
)

View file

@ -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 <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
*
[REDACTED_AUTHOR]
*/
@Composable
fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
SnackbarHost(hostState = hostState, modifier = modifier) {
CopiedTextSnackbar(snackbarData = it)
}
}

View file

@ -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 <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
*
[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)
}
}

View file

@ -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 <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
*
[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)
}
}
}

View file

@ -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<TangemSnackbarModel>(
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
}
}
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

View file

@ -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"
}
}

View file

@ -13,6 +13,7 @@ class PromoResponseConverter : Converter<PromotionInfoResponse, PromoBanner?> {
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),

View file

@ -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<Boolean> {
) : PromoSettingsRepository {
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
}
override fun isReadyToShowTokenPromo(): Flow<Boolean> {
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
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<Boolean> {
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,
)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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<Unit> {
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
}

View file

@ -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,
)
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.card
import arrow.core.Either
interface DeleteSavedAccessCodesUseCase {
suspend operator fun invoke(cardId: String): Either<Throwable, Unit>
}

View file

@ -30,6 +30,12 @@ interface CardTypesResolver {
fun isSatoshiFriendsWallet(): Boolean
fun isBitcoinPizzaDayWallet(): Boolean
fun isVeChainWallet(): Boolean
fun isNewWorldEliteWallet(): Boolean
fun isWhiteWallet(): Boolean
fun isWallet2(): Boolean

View file

@ -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"
}
}

View file

@ -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<Boolean> = swapPromoRepository.isReadyToShowTokenPromo()
operator fun invoke(): Flow<Boolean> = promoSettingsRepository.isReadyToShowTokenSwapPromo()
suspend fun neverToShow() = swapPromoRepository.setNeverToShowTokenPromo()
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowTokenSwapPromo()
}

View file

@ -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<Boolean> = swapPromoRepository.isReadyToShowWalletPromo()
operator fun invoke(): Flow<Boolean> = promoSettingsRepository.isReadyToShowWalletSwapPromo()
suspend fun neverToShow() = swapPromoRepository.setNeverToShowWalletPromo()
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletSwapPromo()
}

View file

@ -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<Boolean> = promoSettingsRepository.isReadyToShowWalletTravalaPromo()
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletTravalaPromo()
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.settings.repositories
import kotlinx.coroutines.flow.Flow
interface PromoSettingsRepository {
fun isReadyToShowWalletSwapPromo(): Flow<Boolean>
fun isReadyToShowTokenSwapPromo(): Flow<Boolean>
suspend fun setNeverToShowWalletSwapPromo()
suspend fun setNeverToShowTokenSwapPromo()
fun isReadyToShowWalletTravalaPromo(): Flow<Boolean>
suspend fun setNeverToShowWalletTravalaPromo()
}

View file

@ -1,13 +0,0 @@
package com.tangem.domain.settings.repositories
import kotlinx.coroutines.flow.Flow
interface SwapPromoRepository {
fun isReadyToShowWalletPromo(): Flow<Boolean>
fun isReadyToShowTokenPromo(): Flow<Boolean>
suspend fun setNeverToShowWalletPromo()
suspend fun setNeverToShowTokenPromo()
}

View file

@ -26,6 +26,7 @@ dependencies {
/** Project - Other */
implementation(projects.core.utils)
implementation(projects.libs.crypto)
/** Android - Other */
implementation(deps.androidx.paging.runtime)

View file

@ -12,6 +12,7 @@ data class PromoBanner(
data class BannerState(
val timeline: Timeline,
val status: String,
val link: String?,
)
data class Timeline(

View file

@ -45,4 +45,6 @@ sealed class CryptoCurrencyWarning {
val startDateTime: DateTime,
val endDateTime: DateTime,
) : CryptoCurrencyWarning()
data object BeaconChainShutdown : CryptoCurrencyWarning()
}

View file

@ -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
}

View file

@ -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<String> =
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 -> ""
}
}
}

View file

@ -5,4 +5,6 @@ import com.tangem.domain.promo.PromoBanner
interface PromoRepository {
suspend fun getChangellyPromoBanner(): PromoBanner?
suspend fun getTravalaPromoBanner(): PromoBanner?
}

View file

@ -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<Unit>
suspend fun sendTransaction(
txData: TransactionData,
signer: CommonSigner,

View file

@ -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<Throwable, Unit> {
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() })
}
}

View file

@ -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<List<UserWallet>> = userWalletsListManager.userWallets
@Throws(IllegalArgumentException::class)
suspend fun invokeSync(): List<UserWallet>? = userWalletsListManager.userWallets.firstOrNull()
}

View file

@ -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

View file

@ -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<ReferralViewModel>()

View file

@ -7,4 +7,7 @@ interface SendFeatureToggles {
/** Availability of redesigned send screen */
val isRedesignedSendEnabled: Boolean
/** Updates remote toggle */
suspend fun fetchNewSendEnabled()
}

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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<Boolean> = 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")
}
}
}

View file

@ -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

View file

@ -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,
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyProvider: Provider<CryptoCurrency>,
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
}
}
}

View file

@ -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,
)

View file

@ -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),
),
)
}
}

View file

@ -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<ImmutableList<SendNotification>> = 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<SendNotification>.addDustWarningNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
private suspend fun MutableList<SendNotification>.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<SendNotification>.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<SendNotification>.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<SendNotification>.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<SendNotification>.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),
),
)
}
}
}

View file

@ -54,7 +54,7 @@ internal fun checkFeeCoverage(
/**
* Calculates subtracted amount
*/
internal fun calculateSubtractedAmount(
private fun calculateSubtractedAmount(
isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,

View file

@ -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,

View file

@ -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,

View file

@ -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<StateRouter>,
@ -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(

View file

@ -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),

View file

@ -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()

View file

@ -15,6 +15,7 @@ internal fun LazyListScope.notifications(
notifications: ImmutableList<SendNotification>,
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,
)
},
)

Some files were not shown because too many files have changed in this diff Show more